diff --git a/packages/contracts-bedrock/audit/decurity/Ozean L2 Contracts Audit Report 2024 1.0.pdf b/packages/contracts-bedrock/audit/decurity/Ozean L2 Contracts Audit Report 2024 1.0.pdf new file mode 100644 index 00000000000..599dc6374ed Binary files /dev/null and b/packages/contracts-bedrock/audit/decurity/Ozean L2 Contracts Audit Report 2024 1.0.pdf differ diff --git a/packages/contracts-bedrock/deployments/deployments.md b/packages/contracts-bedrock/deployments/deployments.md new file mode 100644 index 00000000000..039c2efa624 --- /dev/null +++ b/packages/contracts-bedrock/deployments/deployments.md @@ -0,0 +1,54 @@ +# Ozean Smart Contract Deployments + +## Layer One + +### Mainnet + +TBD + +### Sepolia + +#### Contracts: + +| **Contract** | **Address** | +|:---:|:---:| +| **USDX** | [0x43bd82D1e29a1bEC03AfD11D5a3252779b8c760c](https://sepolia.etherscan.io/token/0x43bd82d1e29a1bec03afd11d5a3252779b8c760c#code)| +| **USDX Bridge** | [0x084C27a0bE5dF26ed47F00678027A6E76B14a0B4](https://sepolia.etherscan.io/address/0x084c27a0be5df26ed47f00678027a6e76b14a0b4#code)| +| **LGE Staking** | [0xc7c0f3b165dec204f6784c9f8b2b148d694d7a32](https://sepolia.etherscan.io/address/0xc7c0f3b165dec204f6784c9f8b2b148d694d7a32#code)| + +#### USDX Bridge Assets/Cap: +| **Asset** | **Address** |**Deposit Cap** | +|:---:|:---:|:---:| +| **USDT** | [0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0](https://sepolia.etherscan.io/address/0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0)| 1_000_000_000_000 | +| **DAI** | [0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357](https://sepolia.etherscan.io/address/0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357)| 1_000_000_000_000 | +| **USDC** | [0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8](https://sepolia.etherscan.io/address/0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8)| 1_000_000_000_000 | +| **USDC** | [0x15795aadca3759d7b356DecE036c285b1FBb32aa](https://sepolia.etherscan.io/address/0x15795aadca3759d7b356DecE036c285b1FBb32aa)| 1_000_000_000_000 | + +#### LGE Staking Assets/Cap: + +| **Asset** | **Address** |**Deposit Cap** | +|:---:|:---:|:---:| +| **wstETH** | [0xB82381A3fBD3FaFA77B3a7bE693342618240067b](https://sepolia.etherscan.io/address/0xB82381A3fBD3FaFA77B3a7bE693342618240067b)| 1_000_000 | +| **WBTC** | [0x29f2D40B0605204364af54EC677bD022dA425d03](https://sepolia.etherscan.io/address/0x29f2D40B0605204364af54EC677bD022dA425d03)| 1_000_000 | +| **USDT** | [0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0](https://sepolia.etherscan.io/address/0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0)| 1_000_000 | +| **DAI** | [0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357](https://sepolia.etherscan.io/address/0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357)| 1_000_000 | +| **USDC** | [0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8](https://sepolia.etherscan.io/address/0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8)| 1_000_000 | +| **AAVE** | [0x88541670E55cC00bEEFD87eB59EDd1b7C511AC9a](https://sepolia.etherscan.io/address/0x88541670E55cC00bEEFD87eB59EDd1b7C511AC9a)| 1_000_000 | + +[**Aave Faucet**](https://app.aave.com/faucet/) + +## Layer Two + +### Ozean Mainnet + +TBD + +### Ozean Poseidon + +#### Contracts: + +| **Contract** | **Address** | +|:---:|:---:| +| **ozUSD Impl (DEPRECATED)** | [0x9e76FE3E3859A4BF1C30d2DAD7b3C35d8654Eb50](https://ozean-testnet.explorer.caldera.xyz/address/0x9e76FE3E3859A4BF1C30d2DAD7b3C35d8654Eb50)| +| **ozUSD Proxy (DEPRECATED)** | [0x1Ce4888a6dED8d6aE5F5D9ca1CABc758c680950b](https://ozean-testnet.explorer.caldera.xyz/address/0x1Ce4888a6dED8d6aE5F5D9ca1CABc758c680950b)| +| **wozUSD** | [0x2f6807b76c426527C3a5C442E8697f12C554195b](https://ozean-testnet.explorer.caldera.xyz/address/0x2f6807b76c426527C3a5C442E8697f12C554195b)| diff --git a/packages/contracts-bedrock/scripts/ozean/LGEMigrationDeploy.s.sol b/packages/contracts-bedrock/scripts/ozean/LGEMigrationDeploy.s.sol new file mode 100644 index 00000000000..4c394629683 --- /dev/null +++ b/packages/contracts-bedrock/scripts/ozean/LGEMigrationDeploy.s.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Script } from "forge-std/Script.sol"; +import { LGEMigrationV1 } from "src/L1/LGEMigrationV1.sol"; +import "forge-std/console.sol"; + +contract LGEMigrationDeploy is Script { + LGEMigrationV1 public lgeMigration; + address public hexTrust; + address public l1StandardBridge; + address public l1LidoTokensBridge; + address public usdxBridge; + address public lgeStaking; + address public usdc; + address public wstETH; + address[] public l1Addresses; + address[] public l2Addresses; + address[] public restrictedL2Addresses; + + /// @dev Used in testing environment, unnecessary for mainnet deployment + function setUp( + address _hexTrust, + address _l1StandardBridge, + address _l1LidoTokensBridge, + address _usdxBridge, + address _lgeStaking, + address _usdc, + address _wstETH, + address[] memory _l1Addresses, + address[] memory _l2Addresses, + address[] memory _restrictedL2Addresses + ) external { + hexTrust = _hexTrust; + l1StandardBridge = _l1StandardBridge; + l1LidoTokensBridge = _l1LidoTokensBridge; + usdxBridge = _usdxBridge; + lgeStaking = _lgeStaking; + usdc = _usdc; + wstETH = _wstETH; + l1Addresses = _l1Addresses; + l2Addresses = _l2Addresses; + restrictedL2Addresses = _restrictedL2Addresses; + } + + function run() external broadcast { + require(hexTrust != address(0), "Script: Zero address."); + require(l1StandardBridge != address(0), "Script: Zero address."); + require(l1LidoTokensBridge != address(0), "Script: Zero address."); + require(usdxBridge != address(0), "Script: Zero address."); + require(lgeStaking != address(0), "Script: Zero address."); + require(usdc != address(0), "Script: Zero address."); + require(wstETH != address(0), "Script: Zero address."); + + uint256 length = l1Addresses.length; + require(length == l2Addresses.length, "Script: Unequal length."); + for (uint256 i; i < length; i++) { + require(l1Addresses[i] != address(0), "Script: Zero address."); + require(l2Addresses[i] != address(0), "Script: Zero address."); + } + + bytes memory deployData = abi.encode(hexTrust, + l1StandardBridge, + l1LidoTokensBridge, + usdxBridge, + lgeStaking, + usdc, + wstETH, + l1Addresses, + l2Addresses, + restrictedL2Addresses + ); + console.logBytes(deployData); + + lgeMigration = new LGEMigrationV1( + hexTrust, + l1StandardBridge, + l1LidoTokensBridge, + usdxBridge, + lgeStaking, + usdc, + wstETH, + l1Addresses, + l2Addresses, + restrictedL2Addresses + ); + } + + modifier broadcast() { + vm.startBroadcast(msg.sender); + _; + vm.stopBroadcast(); + } +} diff --git a/packages/contracts-bedrock/scripts/ozean/LGEStakingDeploy.s.sol b/packages/contracts-bedrock/scripts/ozean/LGEStakingDeploy.s.sol new file mode 100644 index 00000000000..4959132517c --- /dev/null +++ b/packages/contracts-bedrock/scripts/ozean/LGEStakingDeploy.s.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Script } from "forge-std/Script.sol"; +import { LGEStaking } from "src/L1/LGEStaking.sol"; +import "forge-std/console.sol"; + +contract LGEStakingDeploy is Script { + LGEStaking public lgeStaking; + address public stETH; + address public wstETH; + address public hexTrust; + address[] public tokens; + uint256[] public depositCaps; + + /// @dev Used in testing environment, unnecessary for mainnet deployment + function setUp( + address _hexTrust, + address _stETH, + address _wstETH, + address[] memory _tokens, + uint256[] memory _depositCaps + ) external { + hexTrust = _hexTrust; + stETH = _stETH; + wstETH = _wstETH; + tokens = _tokens; + depositCaps = _depositCaps; + } + + function run() external broadcast { + require(hexTrust != address(0), "Script: Zero address."); + require(stETH != address(0), "Script: Zero address."); + require(wstETH != address(0), "Script: Zero address."); + + uint256 length = tokens.length; + require(length == depositCaps.length, "Script: Unequal length."); + for (uint256 i; i < length; i++) { + require(tokens[i] != address(0), "Script: Zero address."); + require(depositCaps[i] != 0, "Script: Zero address."); + } + + bytes memory deployData = abi.encode(hexTrust, stETH, wstETH, tokens, depositCaps); + console.logBytes(deployData); + + lgeStaking = new LGEStaking(hexTrust, stETH, wstETH, tokens, depositCaps); + } + + modifier broadcast() { + vm.startBroadcast(msg.sender); + _; + vm.stopBroadcast(); + } +} diff --git a/packages/contracts-bedrock/scripts/ozean/OzUSDDeploy.s.sol b/packages/contracts-bedrock/scripts/ozean/OzUSDDeploy.s.sol index dc6b363ea7c..f5ca750a9ab 100644 --- a/packages/contracts-bedrock/scripts/ozean/OzUSDDeploy.s.sol +++ b/packages/contracts-bedrock/scripts/ozean/OzUSDDeploy.s.sol @@ -2,23 +2,25 @@ pragma solidity 0.8.15; import { Script } from "forge-std/Script.sol"; -import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import { OzUSD } from "src/L2/OzUSD.sol"; contract OzUSDDeploy is Script { - OzUSD public implementation; - TransparentUpgradeableProxy public proxy; - address public admin = makeAddr("admin"); + OzUSD public ozUSD; + address public hexTrust; uint256 public initialSharesAmount = 1e18; - function run() external broadcast { - /// Deploy implementation - implementation = new OzUSD(); + function setUp(address _hexTrust) external { + hexTrust = _hexTrust; + } + + function run() external payable broadcast { + require(hexTrust != address(0), "Script: Zero address."); + require(initialSharesAmount == 1e18, "Script: Zero amount."); + + ozUSD = new OzUSD{value: initialSharesAmount}(hexTrust, initialSharesAmount); - /// Deploy Proxy - proxy = new TransparentUpgradeableProxy{ value: initialSharesAmount }( - address(implementation), admin, abi.encodeWithSignature("initialize(uint256)", initialSharesAmount) - ); + require(address(ozUSD).balance == 1e18, "Script: Initial supply."); + require(ozUSD.balanceOf(address(0xdead)) == 1e18, "Script: Initial supply."); } modifier broadcast() { diff --git a/packages/contracts-bedrock/scripts/ozean/OzUSDPackage.s.sol b/packages/contracts-bedrock/scripts/ozean/OzUSDPackage.s.sol deleted file mode 100644 index e3649a86ce9..00000000000 --- a/packages/contracts-bedrock/scripts/ozean/OzUSDPackage.s.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.15; - -import { Script } from "forge-std/Script.sol"; -import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import { OzUSD } from "src/L2/OzUSD.sol"; -import { WozUSD } from "src/L2/WozUSD.sol"; - -contract OzUSDPackage is Script { - address public admin = 0xa2ef4A5fB028b4543700AC83e87a0B8b4572202e; - uint256 public initialSharesAmount = 1e18; - - function run() external broadcast() { - /// Deploy implementation - OzUSD implementation = new OzUSD(); - - /// Deploy Proxy - TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy{ value: initialSharesAmount }( - address(implementation), admin, abi.encodeWithSignature("initialize(uint256)", initialSharesAmount) - ); - - /// Deploy wozUSD - WozUSD wozUSD = new WozUSD(OzUSD(payable(proxy))); - } - - modifier broadcast() { - vm.startBroadcast(msg.sender); - _; - vm.stopBroadcast(); - } -} diff --git a/packages/contracts-bedrock/scripts/ozean/USDXBridgeDeploy.s.sol b/packages/contracts-bedrock/scripts/ozean/USDXBridgeDeploy.s.sol index 5499f887cd2..b3014b980aa 100644 --- a/packages/contracts-bedrock/scripts/ozean/USDXBridgeDeploy.s.sol +++ b/packages/contracts-bedrock/scripts/ozean/USDXBridgeDeploy.s.sol @@ -5,10 +5,10 @@ import { Script } from "forge-std/Script.sol"; import { OptimismPortal } from "src/L1/OptimismPortal.sol"; import { SystemConfig } from "src/L1/SystemConfig.sol"; import { USDXBridge } from "src/L1/USDXBridge.sol"; +import "forge-std/console.sol"; contract USDXBridgeDeploy is Script { USDXBridge public usdxBridge; - address public hexTrust; address public usdc; address public usdt; @@ -24,9 +24,7 @@ contract USDXBridgeDeploy is Script { address _dai, OptimismPortal _optimismPortal, SystemConfig _systemConfig - ) - external - { + ) external { hexTrust = _hexTrust; usdc = _usdc; usdt = _usdt; @@ -44,6 +42,20 @@ contract USDXBridgeDeploy is Script { depositCaps[0] = 1e30; depositCaps[1] = 1e30; depositCaps[2] = 1e30; + + require(hexTrust != address(0), "Script: Zero address."); + require(address(optimismPortal) != address(0), "Script: Zero address."); + require(address(systemConfig) != address(0), "Script: Zero address."); + + uint256 length = stablecoins.length; + require(length == depositCaps.length, "Script: Unequal length."); + for (uint256 i; i < length; i++) { + require(stablecoins[i] != address(0), "Script: Zero address."); + } + + bytes memory deployData = abi.encode(hexTrust, optimismPortal, systemConfig, stablecoins, depositCaps); + console.logBytes(deployData); + usdxBridge = new USDXBridge(hexTrust, optimismPortal, systemConfig, stablecoins, depositCaps); } diff --git a/packages/contracts-bedrock/src/L1/LGEMigrationV1.sol b/packages/contracts-bedrock/src/L1/LGEMigrationV1.sol new file mode 100644 index 00000000000..d7b8dc9cd26 --- /dev/null +++ b/packages/contracts-bedrock/src/L1/LGEMigrationV1.sol @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {ILGEMigration} from "./interface/ILGEMigration.sol"; + +/// @title LGE Migration V1 +/// @notice This contract facilitates the migration of staked tokens from the LGE Staking pool +/// on Layer 1 to the Ozean Layer 2. +contract LGEMigrationV1 is Ownable, ILGEMigration, ReentrancyGuard { + using SafeERC20 for IERC20; + + /// @notice The standard bridge contract for Layer 1 to Layer 2 transfers. + IL1StandardBridge public immutable l1StandardBridge; + + /// @notice The L1 lido bridge contract. + IL1LidoTokensBridge public immutable l1LidoTokensBridge; + + /// @notice The L1 USDX bridge that converts USDC into USDX. + IUSDXBridge public immutable usdxBridge; + + /// @notice The address of the LGE Staking contract. + address public immutable lgeStaking; + + /// @notice The address of Circle's USDC. + address public immutable usdc; + + /// @notice The address of Wrapped Staked Ether. + address public immutable wstETH; + + /// @notice A mapping from Layer 1 token addresses to their corresponding Layer 2 addresses. + mapping(address => address) public l1ToL2Addresses; + + /// @notice A mapping that identifies invalid L2 migration address recipients. + mapping(address => bool) public restrictedL2Addresses; + + /// @notice A mapping from Layer 1 token address to the gas limits passed to the bridge contracts. + mapping(address => uint32) public gasLimits; + + constructor( + address _owner, + address _l1StandardBridge, + address _l1LidoTokensBridge, + address _usdxBridge, + address _lgeStaking, + address _usdc, + address _wstETH, + address[] memory _l1Addresses, + address[] memory _l2Addresses, + address[] memory _restrictedL2Addresses + ) { + _transferOwnership(_owner); + l1StandardBridge = IL1StandardBridge(_l1StandardBridge); + l1LidoTokensBridge = IL1LidoTokensBridge(_l1LidoTokensBridge); + usdxBridge = IUSDXBridge(_usdxBridge); + lgeStaking = _lgeStaking; + usdc = _usdc; + wstETH = _wstETH; + uint256 length = _l1Addresses.length; + require( + length == _l2Addresses.length, + "LGE Migration: L1 addresses array length must equal the L2 addresses array length." + ); + for (uint256 i; i < length; ++i) { + l1ToL2Addresses[_l1Addresses[i]] = _l2Addresses[i]; + gasLimits[_l1Addresses[i]] = 21000; + } + length = _restrictedL2Addresses.length; + for (uint256 j; j < length; ++j) { + restrictedL2Addresses[_restrictedL2Addresses[j]] = true; + } + } + + /// @notice This function is called by the LGE Staking contract to facilitate migration of staked tokens from + /// the LGE Staking pool to the Ozean L2. + /// @param _l2Destination The address which will be credited the tokens on Ozean. + /// @param _tokens The tokens being migrated to Ozean from the LGE Staking contract. + /// @param _amounts The amounts of each token to be migrated to Ozean for the _user + function migrate(address _l2Destination, address[] calldata _tokens, uint256[] calldata _amounts) + external + nonReentrant + { + require(msg.sender == lgeStaking, "LGE Migration: Only the staking contract can call this function."); + require(!restrictedL2Addresses[_l2Destination], "LGE Migration: L2 address recipient restricted."); + uint256 length = _tokens.length; + for (uint256 i; i < length; i++) { + require( + l1ToL2Addresses[_tokens[i]] != address(0), "LGE Migration: L2 address not set for migration." + ); + if (_tokens[i] == usdc) { + /// Handle USDC + IERC20(_tokens[i]).safeApprove(address(usdxBridge), _amounts[i]); + usdxBridge.bridge(_tokens[i], _amounts[i], _l2Destination); + } else if (_tokens[i] == wstETH) { + /// Handle wstETH + IERC20(_tokens[i]).safeApprove(address(l1LidoTokensBridge), _amounts[i]); + l1LidoTokensBridge.depositERC20To( + _tokens[i], l1ToL2Addresses[_tokens[i]], _l2Destination, _amounts[i], gasLimits[_tokens[i]], "" + ); + } else { + /// Handle other tokens + IERC20(_tokens[i]).safeApprove(address(l1StandardBridge), _amounts[i]); + l1StandardBridge.depositERC20To( + _tokens[i], l1ToL2Addresses[_tokens[i]], _l2Destination, _amounts[i], gasLimits[_tokens[i]], "" + ); + } + } + } + + /// @notice This function allows the contract owner to recover ERC20 tokens from the contract. + /// @param _token The address of the ERC20 token to recover. + /// @param _amount The amount of tokens to transfer to the recipient. + /// @param _recipient The address that will receive the recovered tokens. + function recoverTokens(address _token, uint256 _amount, address _recipient) external onlyOwner nonReentrant { + IERC20(_token).transfer(_recipient, _amount); + } + + /// @notice This function allows the contract owner to change the gas limit passed to the bridging contracts. + /// @param _token The address of the ERC20 token. + /// @param _limit The new gas limit for bridging the token. + function setGasLimit(address _token, uint32 _limit) external onlyOwner { + gasLimits[_token] = _limit; + } +} + +/// Interfaces /// + +interface IL1StandardBridge { + /// @custom:legacy + /// @notice Deposits some amount of ERC20 tokens into a target account on L2. + /// @param _l1Token Address of the L1 token being deposited. + /// @param _l2Token Address of the corresponding token on L2. + /// @param _to Address of the recipient on L2. + /// @param _amount Amount of the ERC20 to deposit. + /// @param _minGasLimit Minimum gas limit for the deposit message on L2. + /// @param _extraData Optional data to forward to L2. + /// Data supplied here will not be used to execute any code on L2 and is + /// only emitted as extra data for the convenience of off-chain tooling. + function depositERC20To( + address _l1Token, + address _l2Token, + address _to, + uint256 _amount, + uint32 _minGasLimit, + bytes calldata _extraData + ) external; +} + +interface IL1LidoTokensBridge { + function depositERC20To( + address l1Token_, + address l2Token_, + address to_, + uint256 amount_, + uint32 l2Gas_, + bytes calldata data_ + ) external; +} + +interface IUSDXBridge { + function bridge(address _stablecoin, uint256 _amount, address _to) external; +} diff --git a/packages/contracts-bedrock/src/L1/LGEStaking.sol b/packages/contracts-bedrock/src/L1/LGEStaking.sol new file mode 100644 index 00000000000..7f15e9712fb --- /dev/null +++ b/packages/contracts-bedrock/src/L1/LGEStaking.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; +import {ILGEMigration} from "src/L1/interface/ILGEMigration.sol"; + +/// @title LGE Staking +/// @notice This contract facilitates staking of ERC20 tokens and ETH for users and allows migration of staked assets to +/// the Ozean L2. +/// @dev Inspired by https://vscode.blockscan.com/ethereum/0xf047ab4c75cebf0eb9ed34ae2c186f3611aeafa6 +contract LGEStaking is Ownable, ReentrancyGuard, Pausable { + using SafeERC20 for IERC20; + + /// @notice The contract address for Lido's staked ether. + address public immutable stETH; + + /// @notice The contract address for Lido's wrapped staked ether. + /// @dev All ETH deposits are converted to wstETH on deposit. + address public immutable wstETH; + + /// @notice The migration contract that facilitates unstaking and deposits to the Ozean L2. + ILGEMigration public lgeMigration; + + /// @notice Addresses of allow-listed ERC20 tokens. + /// @dev token => allowlisted + mapping(address => bool) public allowlisted; + + /// @notice The total amount of tokens deposited via this contract per allowlisted token address. + /// @dev token => amount + mapping(address => uint256) public totalDeposited; + + /// @notice The limit to the amount that can be minted and bridged per token address. + /// @dev token => amount + mapping(address => uint256) public depositCap; + + /// @notice The amount of tokens each user deposited for each allowlisted token. + /// @dev token => user => amount + mapping(address => mapping(address => uint256)) public balance; + + /// EVENTS /// + + /// @notice An event emitted when a deposit is made by a user. + event Deposit(address indexed _token, uint256 _amount, address indexed _to); + + /// @notice An event emitted when is withdrawal is made by a user. + event Withdraw(address indexed _token, uint256 _amount, address indexed _to); + + /// @notice An event emitted when en ERC20 token is set as allowlisted or not (true if allowlisted, false if + /// removed). + event AllowlistSet(address indexed _coin, bool _set); + + /// @notice An event emitted when the deposit cap for an ERC20 token is modified. + event DepositCapSet(address indexed _coin, uint256 _newDepositCap); + + /// @notice An event emitted when a user migrates deposited assets to Ozean. + event TokensMigrated(address indexed _user, address indexed _l2Destination, address[] _tokens, uint256[] _amounts); + + /// @notice An event emitted when the migration contract is modified. + event MigrationContractSet(address _newContract); + + /// SETUP /// + + constructor( + address _owner, + address _stETH, + address _wstETH, + address[] memory _tokens, + uint256[] memory _depositCaps + ) { + _transferOwnership(_owner); + stETH = _stETH; + wstETH = _wstETH; + IstETH(stETH).approve(wstETH, ~uint256(0)); + uint256 length = _tokens.length; + require( + length == _depositCaps.length, "LGE Staking: Tokens array length must equal the Deposit Caps array length." + ); + for (uint256 i; i < length; ++i) { + require(!allowlisted[_tokens[i]], "LGE Staking: Duplicate tokens."); + allowlisted[_tokens[i]] = true; + emit AllowlistSet(_tokens[i], true); + depositCap[_tokens[i]] = _depositCaps[i]; + emit DepositCapSet(_tokens[i], _depositCaps[i]); + } + } + + /// DEPOSIT /// + + /// @notice Deposits ERC20 tokens into the staking contract. + /// @param _token The address of the ERC20 token to deposit. + /// @param _amount The amount of tokens to deposit. + /// @dev Users must grant approval for the contract to move their tokens. + function depositERC20(address _token, uint256 _amount) external nonReentrant whenNotPaused { + require(!migrationActivated(), "LGE Staking: May not deposit once migration has been activated."); + require(_amount > 0, "LGE Staking: May not deposit nothing."); + require(allowlisted[_token], "LGE Staking: Token must be allowlisted."); + require( + totalDeposited[_token] + _amount <= depositCap[_token], "LGE Staking: deposit amount exceeds deposit cap." + ); + uint256 balanceBefore = IERC20(_token).balanceOf(address(this)); + IERC20(_token).safeTransferFrom(msg.sender, address(this), _amount); + require( + IERC20(_token).balanceOf(address(this)) - balanceBefore == _amount, + "LGE Staking: Fee-on-transfer tokens not supported." + ); + balance[_token][msg.sender] += _amount; + totalDeposited[_token] += _amount; + emit Deposit(_token, _amount, msg.sender); + } + + /// @notice Deposits ETH into the staking contract, converting it to wstETH. + /// @dev All ETH is converted to wstETH on deposit. + function depositETH() external payable nonReentrant whenNotPaused { + require(!migrationActivated(), "LGE Staking: May not deposit once migration has been activated."); + require(msg.value > 0, "LGE Staking: May not deposit nothing."); + require(allowlisted[wstETH], "LGE Staking: Token must be allowlisted."); + IstETH(stETH).submit{value: msg.value}(address(0)); + uint256 wstETHAmount = IwstETH(wstETH).wrap(IstETH(stETH).balanceOf(address(this))); + require( + totalDeposited[wstETH] + wstETHAmount <= depositCap[wstETH], + "LGE Staking: deposit amount exceeds deposit cap." + ); + balance[wstETH][msg.sender] += wstETHAmount; + totalDeposited[wstETH] += wstETHAmount; + emit Deposit(wstETH, wstETHAmount, msg.sender); + } + + /// WITHDRAW /// + + /// @notice Withdraws ERC20 tokens from the staking contract. + /// @param _token The address of the ERC20 token to withdraw. + /// @param _amount The amount of tokens to withdraw. + function withdraw(address _token, uint256 _amount) external nonReentrant whenNotPaused { + require(_amount > 0, "LGE Staking: may not withdraw nothing."); + require(balance[_token][msg.sender] >= _amount, "LGE Staking: insufficient deposited balance."); + balance[_token][msg.sender] -= _amount; + totalDeposited[_token] -= _amount; + IERC20(_token).safeTransfer(msg.sender, _amount); + emit Withdraw(_token, _amount, msg.sender); + } + + /// MIGRATE /// + + /// @notice Migrates assets to the specified L2 destination. + /// @param _l2Destination The address of the L2 destination to migrate tokens to. + /// @param _tokens An array of token addresses to migrate. + /// @dev Sends assets to the migration contract, and then calls `migrate` to move the assets. + function migrate(address _l2Destination, address[] calldata _tokens) external nonReentrant whenNotPaused { + require(migrationActivated(), "LGE Staking: Migration not active."); + require(_l2Destination != address(0), "LGE Staking: May not send tokens to the zero address."); + uint256 length = _tokens.length; + require(length > 0, "LGE Staking: Must migrate some tokens."); + uint256[] memory amounts = new uint256[](length); + uint256 amount; + for (uint256 i; i < length; i++) { + amount = balance[_tokens[i]][msg.sender]; + require(amount > 0, "LGE Staking: No tokens to migrate."); + balance[_tokens[i]][msg.sender] -= amount; + totalDeposited[_tokens[i]] -= amount; + amounts[i] = amount; + IERC20(_tokens[i]).safeTransfer(address(lgeMigration), amount); + } + lgeMigration.migrate(_l2Destination, _tokens, amounts); + emit TokensMigrated(msg.sender, _l2Destination, _tokens, amounts); + } + + /// OWNER /// + + /// @notice This function allows the owner to either add or remove an allow-listed token for deposit. + /// @param _token The token address to add or remove. + /// @param _set A boolean for whether the token is allow-listed or not. True for allow-listed, false otherwise. + function setAllowlist(address _token, bool _set) external onlyOwner { + allowlisted[_token] = _set; + emit AllowlistSet(_token, _set); + } + + /// @notice This function allows the owner to modify the deposit cap for deposited tokens. + /// @param _token The token address to modify the deposit cap. + /// @param _newDepositCap The new deposit cap. + function setDepositCap(address _token, uint256 _newDepositCap) external onlyOwner { + depositCap[_token] = _newDepositCap; + emit DepositCapSet(_token, _newDepositCap); + } + + /// @notice This function allows the owner to set the migration contract used to move deposited assets to the + /// Ozean L2. + /// @param _contract The new contract address for the LGE Migration logic. + /// @dev The new migration contract must conform to the ILGEMigration interface. + /// @dev If this contract is set to address(0) migration is deactivated + function setMigrationContract(address _contract) external onlyOwner { + lgeMigration = ILGEMigration(_contract); + emit MigrationContractSet(_contract); + } + + /// @notice This function allows the owner to pause or unpause this contract. + /// @param _set The boolean for whether the contract is to be paused or unpaused. True for paused, false otherwise. + function setPaused(bool _set) external onlyOwner { + _set ? _pause() : _unpause(); + } + + /// VIEW /// + + /// @notice Checks if migration has been activated. + /// @return activated A boolean indicating whether migration is active. + function migrationActivated() public view returns (bool activated) { + activated = (address(lgeMigration) != address(0)); + } +} + +interface IstETH is IERC20 { + function submit(address _referral) external payable returns (uint256); +} + +interface IwstETH is IERC20 { + function wrap(uint256 _stETHAmount) external returns (uint256); +} diff --git a/packages/contracts-bedrock/src/L1/USDXBridge.sol b/packages/contracts-bedrock/src/L1/USDXBridge.sol index 6530e6dfd4f..27198eae85a 100644 --- a/packages/contracts-bedrock/src/L1/USDXBridge.sol +++ b/packages/contracts-bedrock/src/L1/USDXBridge.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import { OptimismPortal } from "src/L1/OptimismPortal.sol"; -import { SystemConfig } from "src/L1/SystemConfig.sol"; -import { ISemver } from "src/universal/ISemver.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {OptimismPortal} from "src/L1/OptimismPortal.sol"; +import {SystemConfig} from "src/L1/SystemConfig.sol"; /// @title USDX Bridge /// @notice This contract provides bridging functionality for allow-listed stablecoins to the Ozean Layer L2. @@ -15,13 +14,9 @@ import { ISemver } from "src/universal/ISemver.sol"; /// the L2 via the Optimism Portal contract. The owner of this contract can modify the set of /// allow-listed stablecoins accepted, along with the deposit caps, and can also withdraw any deposited /// ERC20 tokens. -contract USDXBridge is Ownable, ReentrancyGuard, ISemver { +contract USDXBridge is Ownable, ReentrancyGuard { using SafeERC20 for IERC20Decimals; - /// @notice Semantic version. - /// @custom:semver 1.0.0 - string public constant version = "1.0.0"; - /// @notice Contract of the Optimism Portal. /// @custom:network-specific OptimismPortal public immutable portal; @@ -41,18 +36,27 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { /// @dev stablecoin => amount mapping(address => uint256) public totalBridged; + /// @notice The gas limit passed to the Optimism portal when depositing USDX. + uint64 public gasLimit; + + /// EVENTS /// + /// @notice An event emitted when a bridge deposit is made by a user. event BridgeDeposit(address indexed _stablecoin, uint256 _amount, address indexed _to); /// @notice An event emitted when an ERC20 token is withdrawn from this contract. event WithdrawCoins(address indexed _coin, uint256 _amount, address indexed _to); - /// @notice An event emitted when en ERC20 stablecoin is set as allowlisted or not (true if allowlisted, false if removed). + /// @notice An event emitted when en ERC20 stablecoin is set as allowlisted or not (true if allowlisted, false if + /// removed). event AllowlistSet(address indexed _coin, bool _set); /// @notice An event emitted when the deposit cap for an ERC20 stablecoin is modified. event DepositCapSet(address indexed _coin, uint256 _newDepositCap); + /// @notice An event emitted when the gas limit is updated. + event GasLimitSet(uint64 _newGasLimit); + /// SETUP /// /// @notice The constructor contract set up. @@ -74,14 +78,16 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { _transferOwnership(_owner); portal = _portal; config = _config; + gasLimit = 21000; /// Add allow-listed stablecoins and deposit caps if (address(config) != address(0)) { uint256 length = _stablecoins.length; require( length == _depositCaps.length, - "USDXBridge: Stablecoins array length must equal the Deposit Caps array length." + "USDX Bridge: Stablecoins array length must equal the Deposit Caps array length." ); for (uint256 i; i < length; ++i) { + require(_stablecoins[i] != address(0), "USDX Bridge: Zero address."); allowlisted[_stablecoins[i]] = true; emit AllowlistSet(_stablecoins[i], true); depositCap[_stablecoins[i]] = _depositCaps[i]; @@ -98,16 +104,21 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { /// @param _to Recieving address on L2. function bridge(address _stablecoin, uint256 _amount, address _to) external nonReentrant { /// Checks - require(allowlisted[_stablecoin], "USDXBridge: Stablecoin not accepted."); - require(_amount > 0, "USDXBridge: May not bridge nothing."); + require(allowlisted[_stablecoin], "USDX Bridge: Stablecoin not accepted."); + require(_amount > 0, "USDX Bridge: May not bridge nothing."); uint256 bridgeAmount = _getBridgeAmount(_stablecoin, _amount); require( - totalBridged[_stablecoin] + bridgeAmount < depositCap[_stablecoin], - "USDXBridge: Bridge amount exceeds deposit cap." + totalBridged[_stablecoin] + bridgeAmount <= depositCap[_stablecoin], + "USDX Bridge: Bridge amount exceeds deposit cap." ); /// Update state - totalBridged[_stablecoin] += bridgeAmount; + uint256 balanceBefore = IERC20Decimals(_stablecoin).balanceOf(address(this)); IERC20Decimals(_stablecoin).safeTransferFrom(msg.sender, address(this), _amount); + require( + IERC20Decimals(_stablecoin).balanceOf(address(this)) - balanceBefore == _amount, + "USDX Bridge: Fee-on-transfer tokens not supported." + ); + totalBridged[_stablecoin] += bridgeAmount; /// Mint USDX usdx().mint(address(this), bridgeAmount); /// Bridge USDX @@ -116,8 +127,7 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { _to: _to, _mint: bridgeAmount, _value: bridgeAmount, - _gasLimit: 21000, - /// @dev portal.minimumGasLimit(0) + _gasLimit: gasLimit, _isCreation: false, _data: "" }); @@ -143,6 +153,13 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { emit DepositCapSet(_stablecoin, _newDepositCap); } + /// @notice This function allows the owner to modify the gas limit for USDX deposits. + /// @param _newGasLimit The new gas limit to be set for transactions. + function setGasLimit(uint64 _newGasLimit) external onlyOwner { + gasLimit = _newGasLimit; + emit GasLimitSet(_newGasLimit); + } + /// @notice This function allows the owner to withdraw any ERC20 token held by this contract. /// @param _coin The address of the ERC20 token to withdraw. /// @param _amount The amount of tokens to withdraw. @@ -172,13 +189,13 @@ contract USDXBridge is Ownable, ReentrancyGuard, ISemver { } } -/// @notice An interface whihc extends the IERC20 to include a decimals view function. +/// @notice An interface which extends the IERC20 to include a decimals view function. /// @dev Any allow-listed stablecoin added to the bridge must conform to this interface. interface IERC20Decimals is IERC20 { function decimals() external view returns (uint8); } -/// @notice An interface whihc extends the IERC20Decimals to include a mint function to allow for minting +/// @notice An interface which extends the IERC20Decimals to include a mint function to allow for minting /// of new USDX tokens by this bridge. interface IUSDX is IERC20Decimals { function mint(address to, uint256 amount) external; diff --git a/packages/contracts-bedrock/src/L1/interface/ILGEMigration.sol b/packages/contracts-bedrock/src/L1/interface/ILGEMigration.sol new file mode 100644 index 00000000000..798e3f46ede --- /dev/null +++ b/packages/contracts-bedrock/src/L1/interface/ILGEMigration.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/// @title LGE Migration Interface +/// @notice Interface for the LGE Migrator contract to move LGE assets onto Ozean mainnet. +interface ILGEMigration { + function migrate(address _l2Destination, address[] calldata _tokens, uint256[] calldata _amounts) external; +} diff --git a/packages/contracts-bedrock/src/L2/OzUSD.sol b/packages/contracts-bedrock/src/L2/OzUSD.sol index e400fdd119e..42af15bb277 100644 --- a/packages/contracts-bedrock/src/L2/OzUSD.sol +++ b/packages/contracts-bedrock/src/L2/OzUSD.sol @@ -1,21 +1,32 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {Pausable} from "@openzeppelin/contracts/security/Pausable.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; /// @title Ozean USD (ozUSD) Token Contract /// @notice This contract implements a rebasing token (ozUSD), where token balances are dynamic and calculated /// based on shares controlled by each account. The total pooled USDX (protocol-controlled USDX) determines the /// total balances; meaning that any USDX sent to this contract automatically rebases all user balances. /// 1 USDX == 1 ozUSD. -/// @dev This contract does not fully comply with the ERC20 standard as rebasing events do not emit `Transfer` events. -/// This contract is inspired by Lido's stETH contract: https://vscode.blockscan.com/ethereum/0x17144556fd3424edc8fc8a4c940b2d04936d17eb -contract OzUSD is IERC20, ReentrancyGuard, Initializable { +/// @dev This contract does not fully comply with the ERC20 standard as rebasing events do not emit `Transfer` +/// events. +/// This contract is inspired by Lido's stETH contract: +/// https://vscode.blockscan.com/ethereum/0x17144556fd3424edc8fc8a4c940b2d04936d17eb +contract OzUSD is IERC20, ReentrancyGuard, Pausable, Ownable { + /// @notice The name of the token, Ozean USD. string public constant name = "Ozean USD"; + + /// @notice The symbol of the token, ozUSD. string public constant symbol = "ozUSD"; + + /// @notice The number of decimals the token uses, 18. uint8 public constant decimals = 18; + + /// @notice Total number of shares in circulation for ozUSD. + /// @dev This is used to calculate the rebased ozUSD balances. uint256 private totalShares; /// @notice A mapping from addresses to shares controlled by each account. @@ -47,25 +58,23 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount ); - /// SETUP /// + /// @notice An event for distribution of yield (in the form of USDX) to all participants. + /// @param _previousTotalBalance The total amount of USDX held by the contract before rebasing. + /// @param _newTotalBalance The total amount of USDX held by the contract after rebasing. + event YieldDistributed(uint256 _previousTotalBalance, uint256 _newTotalBalance); - constructor() { - _disableInitializers(); - } + /// SETUP /// - /// @notice Initializes the contract with a specific amount of shares. - /// @dev Requires the sender to send USDX equal to the number of shares specified in `_sharesAmount`. - /// @param _sharesAmount The number of shares to initialize. - function initialize(uint256 _sharesAmount) external payable initializer nonReentrant { - require(msg.value == _sharesAmount, "OzUSD: INCORRECT_VALUE"); + constructor(address _owner, uint256 _sharesAmount) payable { + _transferOwnership(_owner); + require(msg.value >= 1 ether, "OzUSD: Must deploy with at least one USDX."); + require(msg.value == _sharesAmount, "OzUSD: Incorrect value."); _mintShares(address(0xdead), _sharesAmount); _emitTransferAfterMintingShares(address(0xdead), _sharesAmount); } /// EXTERNAL /// - receive() external payable { } - /// @notice Transfers an amount of ozUSD tokens from the caller to a recipient. /// @param _recipient The recipient of the token transfer. /// @param _amount The number of ozUSD tokens to transfer. @@ -94,7 +103,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { /// @param _spender The address authorized to spend the tokens. /// @param _amount The number of tokens allowed to be spent. /// @return success Returns `true` if the approval was successful. - /// @dev The `_amount` argument is the amount of tokens, not shares. + /// @dev The `_amount` argument is the amount of tokens, not shares. function approve(address _spender, uint256 _amount) external nonReentrant returns (bool) { _approve(msg.sender, _spender, _amount); return true; @@ -118,7 +127,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { /// Reverts if the current allowance is less than the amount being subtracted. function decreaseAllowance(address _spender, uint256 _subtractedValue) external nonReentrant returns (bool) { uint256 currentAllowance = allowances[msg.sender][_spender]; - require(currentAllowance >= _subtractedValue, "OzUSD: ALLOWANCE_BELOW_ZERO"); + require(currentAllowance >= _subtractedValue, "OzUSD: Allowance below value."); _approve(msg.sender, _spender, currentAllowance - _subtractedValue); return true; } @@ -136,17 +145,12 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { } /// @notice Transfers `_sharesAmount` shares from `_sender` to `_recipient` and returns the equivalent ozUSD tokens. - /// @dev Shares are transferred, and equivalent ozUSD tokens are calculated and returned. /// @param _sender The address to transfer shares from. /// @param _recipient The address to transfer shares to. /// @param _sharesAmount The number of shares to transfer. /// @return uint256 The amount of ozUSD tokens equivalent to the transferred shares. /// @dev The `_sharesAmount` argument is the amount of shares, not tokens. - function transferSharesFrom( - address _sender, - address _recipient, - uint256 _sharesAmount - ) + function transferSharesFrom(address _sender, address _recipient, uint256 _sharesAmount) external nonReentrant returns (uint256) @@ -162,33 +166,46 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { /// @dev Transfers USDX and mints new shares accordingly. /// @param _to The address to receive the minted ozUSD. /// @param _usdxAmount The amount of USDX to lock in exchange for ozUSD. - function mintOzUSD(address _to, uint256 _usdxAmount) external payable nonReentrant { - require(_usdxAmount != 0, "OzUSD: Amount zero"); - require(msg.value == _usdxAmount, "OzUSD: Insufficient USDX transfer"); - + function mintOzUSD(address _to, uint256 _usdxAmount) external payable nonReentrant whenNotPaused { + require(_usdxAmount != 0, "OzUSD: Amount zero."); + require(msg.value == _usdxAmount, "OzUSD: Insufficient USDX transfer."); /// @dev Have to minus `_usdxAmount` from denominator given the transfer of funds has already occured uint256 sharesToMint = (_usdxAmount * totalShares) / (_getTotalPooledUSDX() - _usdxAmount); - uint256 newTotalShares = _mintShares(_to, sharesToMint); - - _emitTransferAfterMintingShares(_to, newTotalShares); + _mintShares(_to, sharesToMint); + _emitTransferAfterMintingShares(_to, sharesToMint); } /// @notice Redeems ozUSD tokens by burning shares and redeeming the equivalent amount of `_ozUSDAmount` in USDX. /// @param _from The address that owns the ozUSD to redeem. /// @param _ozUSDAmount The amount of ozUSD to redeem. - /// @dev Spender must approve contract, even if owner of coins - /// Burns shares and transfers back the corresponding USDX. + /// @dev Burns shares and transfers back the corresponding USDX. function redeemOzUSD(address _from, uint256 _ozUSDAmount) external nonReentrant { - require(_ozUSDAmount != 0, "OzUSD: Amount zero"); - _spendAllowance(_from, msg.sender, _ozUSDAmount); - + require(_ozUSDAmount != 0, "OzUSD: Amount zero."); + if (msg.sender != _from) _spendAllowance(_from, msg.sender, _ozUSDAmount); uint256 sharesToBurn = getSharesByPooledUSDX(_ozUSDAmount); _burnShares(_from, sharesToBurn); + (bool success,) = _from.call{value: _ozUSDAmount}(""); + require(success, "OzUSD: Transfer Failed."); + _emitTransferEvents(_from, address(0), _ozUSDAmount, sharesToBurn); + } - (bool s,) = _from.call{ value: _ozUSDAmount }(""); - assert(s); + receive() external payable { + require(msg.value >= 1 ether, "OzUSD: Must distribute at least one USDX."); + emit YieldDistributed(_getTotalPooledUSDX() - msg.value, _getTotalPooledUSDX()); + } + + /// OWNER /// - _emitTransferEvents(msg.sender, address(0), _ozUSDAmount, sharesToBurn); + /// @notice Distributes the yield to the protocol by updating the total pooled USDX balance. + function distributeYield() external payable nonReentrant onlyOwner { + (bool success,) = address(this).call{value: msg.value}(""); + require(success, "OzUSD: Transfer failed."); + } + + /// @notice This function allows the owner to pause or unpause this contract. + /// @param _set The boolean for whether the contract is to be paused or unpaused. True for paused, false otherwise. + function setPaused(bool _set) external onlyOwner { + _set ? _pause() : _unpause(); } /// VIEW /// @@ -245,11 +262,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { return address(this).balance; } - function _sharesOf(address _account) internal view returns (uint256) { - return shares[_account]; - } - - /// @dev Moves `_amount` tokens from `_sender` to `_recipient`. + /// @dev Moves `_amount` tokens from `_sender` to `_recipient`. function _transfer(address _sender, address _recipient, uint256 _amount) internal { uint256 _sharesToTransfer = getSharesByPooledUSDX(_amount); _transferShares(_sender, _recipient, _sharesToTransfer); @@ -257,9 +270,8 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { } function _approve(address _owner, address _spender, uint256 _amount) internal { - require(_owner != address(0), "OzUSD: APPROVE_FROM_ZERO_ADDR"); - require(_spender != address(0), "OzUSD: APPROVE_TO_ZERO_ADDR"); - + require(_owner != address(0), "OzUSD: Approve from zero address."); + require(_spender != address(0), "OzUSD: Approve to zero address."); allowances[_owner][_spender] = _amount; emit Approval(_owner, _spender, _amount); } @@ -267,19 +279,18 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { function _spendAllowance(address _owner, address _spender, uint256 _amount) internal { uint256 currentAllowance = allowances[_owner][_spender]; if (currentAllowance != ~uint256(0)) { - require(currentAllowance >= _amount, "OzUSD: ALLOWANCE_EXCEEDED"); + require(currentAllowance >= _amount, "OzUSD: Allowance exceeded."); _approve(_owner, _spender, currentAllowance - _amount); } } function _transferShares(address _sender, address _recipient, uint256 _sharesAmount) internal { - require(_sender != address(0), "OzUSD: TRANSFER_FROM_ZERO_ADDR"); - require(_recipient != address(0), "OzUSD: TRANSFER_TO_ZERO_ADDR"); - require(_recipient != address(this), "OzUSD: TRANSFER_TO_STETH_CONTRACT"); - + require(_sharesAmount != 0, "OzUSD: Transfer zero shares."); + require(_sender != address(0), "OzUSD: Transfer from zero address."); + require(_recipient != address(0), "OzUSD: Transfer to zero address."); + require(_recipient != address(this), "OzUSD: Transfer to this contract."); uint256 currentSenderShares = shares[_sender]; - require(_sharesAmount <= currentSenderShares, "OzUSD: BALANCE_EXCEEDED"); - + require(_sharesAmount <= currentSenderShares, "OzUSD: Balance exceeded."); shares[_sender] = currentSenderShares - _sharesAmount; shares[_recipient] = shares[_recipient] + _sharesAmount; } @@ -287,8 +298,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { /// @notice Creates `_sharesAmount` shares and assigns them to `_recipient`, increasing the total amount of shares. /// @dev This doesn't increase the token total supply. function _mintShares(address _recipient, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { - require(_recipient != address(0), "OzUSD: MINT_TO_ZERO_ADDR"); - + require(_recipient != address(0), "OzUSD: Mint to zero address."); newTotalShares = totalShares + _sharesAmount; totalShares = newTotalShares; shares[_recipient] += _sharesAmount; @@ -297,19 +307,15 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { /// @notice Destroys `_sharesAmount` shares from `_account`'s holdings, decreasing the total amount of shares. /// @dev This doesn't decrease the token total supply. function _burnShares(address _account, uint256 _sharesAmount) internal returns (uint256 newTotalShares) { - require(_account != address(0), "OzUSD: BURN_FROM_ZERO_ADDR"); - + require(_sharesAmount != 0, "OzUSD: Burn zero shares."); + require(_account != address(0), "OzUSD: Burn from zero address."); uint256 accountShares = shares[_account]; - require(_sharesAmount <= accountShares, "OzUSD: BALANCE_EXCEEDED"); - + require(_sharesAmount <= accountShares, "OzUSD: Balance exceeded."); uint256 preRebaseTokenAmount = getPooledUSDXByShares(_sharesAmount); - newTotalShares = totalShares - _sharesAmount; totalShares = newTotalShares; shares[_account] = accountShares - _sharesAmount; - uint256 postRebaseTokenAmount = getPooledUSDXByShares(_sharesAmount); - emit SharesBurnt(_account, preRebaseTokenAmount, postRebaseTokenAmount, _sharesAmount); } @@ -318,7 +324,7 @@ contract OzUSD is IERC20, ReentrancyGuard, Initializable { emit TransferShares(_from, _to, _sharesAmount); } - /// @dev Emits {Transfer} and {TransferShares} events where `from` is 0 address. Indicates mint events. + /// @dev Emits {Transfer} and {TransferShares} events where `from` is 0 address. Indicates mint events. function _emitTransferAfterMintingShares(address _to, uint256 _sharesAmount) internal { _emitTransferEvents(address(0), _to, getPooledUSDXByShares(_sharesAmount), _sharesAmount); } diff --git a/packages/contracts-bedrock/src/L2/WozUSD.sol b/packages/contracts-bedrock/src/L2/WozUSD.sol index 73e20558842..4e62e77e64d 100644 --- a/packages/contracts-bedrock/src/L2/WozUSD.sol +++ b/packages/contracts-bedrock/src/L2/WozUSD.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import { OzUSD } from "./OzUSD.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import {OzUSD} from "./OzUSD.sol"; /// @title Wrapped Ozean USD (WozUSD) /// @notice A wrapper contract for OzUSD, providing auto-compounding functionality. /// @dev The contract wraps ozUSD into wozUSD, which represents shares of ozUSD. -/// This contract is inspired by Lido's wstETH contract: https://vscode.blockscan.com/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 +/// This contract is inspired by Lido's wstETH contract: +/// https://vscode.blockscan.com/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 contract WozUSD is ERC20, ReentrancyGuard { - /// @notice The instance of the ozUSD proxy contract. + /// @notice The instance of the ozUSD contract. OzUSD public immutable ozUSD; constructor(OzUSD _ozUSD) ERC20("Wrapped Ozean USD", "wozUSD") { diff --git a/packages/contracts-bedrock/test/L1/LGEStaking.t.sol b/packages/contracts-bedrock/test/L1/LGEStaking.t.sol new file mode 100644 index 00000000000..73b0f30a8f7 --- /dev/null +++ b/packages/contracts-bedrock/test/L1/LGEStaking.t.sol @@ -0,0 +1,605 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { console2 as console } from "forge-std/console2.sol"; +import { CommonTest } from "test/setup/CommonTest.sol"; +import { LGEStakingDeploy } from "scripts/ozean/LGEStakingDeploy.s.sol"; +import { LGEMigrationDeploy } from "scripts/ozean/LGEMigrationDeploy.s.sol"; +import { LGEStaking } from "src/L1/LGEStaking.sol"; +import { LGEMigrationV1 } from "src/L1/LGEMigrationV1.sol"; +import { TestERC20Decimals, TestERC20DecimalsFeeOnTransfer } from "test/mocks/TestERC20.sol"; +import { MockUSDX } from "test/mocks/MockUSDX.sol"; +import { TestStETH, TestWstETH } from "test/mocks/TestLido.sol"; + +/// @dev forge test --match-contract LGEStakingTest +contract LGEStakingTest is CommonTest { + LGEStaking public lgeStaking; + LGEMigrationV1 public lgeMigration; + address public hexTrust; + + /// 18 decimals + TestERC20Decimals public wBTC; + TestERC20Decimals public solvBTC; + TestERC20Decimals public lombardBTC; + TestERC20Decimals public wSOL; + TestERC20Decimals public sUSDe; + TestERC20Decimals public USDe; + TestERC20Decimals public AUSD; + TestERC20Decimals public USDY; + TestERC20Decimals public USDM; + TestERC20Decimals public sDAI; + /// 6 decimals + TestERC20Decimals public USDC; + + /// Mock Lido contracts + TestStETH public stETH; + TestWstETH public wstETH; + + /// @dev mock these too? + address public l1LidoTokensBridge; + address public usdxBridge; + + address[] public l1Addresses; + address[] public l2Addresses; + address[] public restrictedL2Addresses; + uint256[] public depositCaps; + + /// LGEStaking events + event Deposit(address indexed _token, uint256 _amount, address indexed _to); + event Withdraw(address indexed _token, uint256 _amount, address indexed _to); + event AllowlistSet(address indexed _coin, bool _set); + event DepositCapSet(address indexed _coin, uint256 _newDepositCap); + event TokensMigrated(address indexed _user, address indexed _l2Destination, address[] _tokens, uint256[] _amounts); + event MigrationContractSet(address _newContract); + /// Pausable events + event Paused(address account); + event Unpaused(address account); + + function setUp() public override { + /// Deploy Ozean + super.setUp(); + + /// Set up environment + /// @dev Hex Trust treated as owner of both lgeStaking and lgeMigration + hexTrust = makeAddr("HEX_TRUST"); + wBTC = new TestERC20Decimals{ salt: bytes32("wBTC") }(18); + solvBTC = new TestERC20Decimals{ salt: bytes32("solvBTC") }(18); + lombardBTC = new TestERC20Decimals{ salt: bytes32("lombardBTC") }(18); + wSOL = new TestERC20Decimals{ salt: bytes32("wSOL") }(18); + sUSDe = new TestERC20Decimals{ salt: bytes32("sUSDe") }(18); + USDe = new TestERC20Decimals{ salt: bytes32("USDe") }(18); + AUSD = new TestERC20Decimals{ salt: bytes32("AUSD") }(18); + USDY = new TestERC20Decimals{ salt: bytes32("USDY") }(18); + USDM = new TestERC20Decimals{ salt: bytes32("USDM") }(18); + sDAI = new TestERC20Decimals{ salt: bytes32("sDAI") }(18); + USDC = new TestERC20Decimals{ salt: bytes32("USDC") }(6); + usdx = new MockUSDX(); + stETH = new TestStETH(); + wstETH = new TestWstETH(address(stETH)); + + /// @dev placeholders + l1LidoTokensBridge = address(1); + usdxBridge = address(1); + + /// Deploy LGEStaking + l1Addresses = new address[](13); + l1Addresses[0] = address(wBTC); + l1Addresses[1] = address(solvBTC); + l1Addresses[2] = address(lombardBTC); + l1Addresses[3] = address(wSOL); + l1Addresses[4] = address(wstETH); + l1Addresses[5] = address(sUSDe); + l1Addresses[6] = address(USDe); + l1Addresses[7] = address(AUSD); + l1Addresses[8] = address(USDY); + l1Addresses[9] = address(USDM); + l1Addresses[10] = address(sDAI); + l1Addresses[11] = address(USDC); + l1Addresses[12] = address(usdx); + + depositCaps = new uint256[](13); + depositCaps[0] = 1e30; + depositCaps[1] = 1e30; + depositCaps[2] = 1e30; + depositCaps[3] = 1e30; + depositCaps[4] = 1e30; + depositCaps[5] = 1e30; + depositCaps[6] = 1e30; + depositCaps[7] = 1e30; + depositCaps[8] = 1e30; + depositCaps[9] = 1e30; + depositCaps[10] = 1e30; + depositCaps[11] = 1e30; + depositCaps[12] = 1e30; + + LGEStakingDeploy stakingDeployScript = new LGEStakingDeploy(); + stakingDeployScript.setUp(hexTrust, address(stETH), address(wstETH), l1Addresses, depositCaps); + stakingDeployScript.run(); + lgeStaking = stakingDeployScript.lgeStaking(); + + /// Deploy LGEMigration + /// @dev not the correct L2 address + l2Addresses = new address[](13); + l2Addresses[0] = address(wBTC); + l2Addresses[1] = address(wBTC); + l2Addresses[2] = address(wBTC); + l2Addresses[3] = address(wBTC); + l2Addresses[4] = address(wBTC); + l2Addresses[5] = address(wBTC); + l2Addresses[6] = address(wBTC); + l2Addresses[7] = address(wBTC); + l2Addresses[8] = address(wBTC); + l2Addresses[9] = address(wBTC); + l2Addresses[10] = address(wBTC); + l2Addresses[11] = address(wBTC); + l2Addresses[12] = address(wBTC); + + /// @dev abitrary restricted L2 destinations + restrictedL2Addresses = new address[](2); + restrictedL2Addresses[0] = address(1000); + restrictedL2Addresses[1] = address(1001); + + LGEMigrationDeploy migrationDeployScript = new LGEMigrationDeploy(); + migrationDeployScript.setUp( + hexTrust, + address(l1StandardBridge), + address(l1LidoTokensBridge), + address(usdxBridge), + address(lgeStaking), + address(USDC), + address(wstETH), + l1Addresses, + l2Addresses, + restrictedL2Addresses + ); + migrationDeployScript.run(); + lgeMigration = migrationDeployScript.lgeMigration(); + } + + /// SETUP /// + + function testInitialize() public view { + assertEq(address(lgeStaking.lgeMigration()), address(0)); + assertEq(lgeStaking.migrationActivated(), false); + + for (uint256 i; i < 13; i++) { + assertEq(lgeStaking.allowlisted(l1Addresses[i]), true); + assertEq(lgeStaking.depositCap(l1Addresses[i]), 1e30); + assertEq(lgeStaking.totalDeposited(l1Addresses[i]), 0); + } + } + + function testDeployRevertWithUnequalArrayLengths() public { + l1Addresses = new address[](3); + l1Addresses[0] = address(wBTC); + l1Addresses[1] = address(solvBTC); + l1Addresses[2] = address(lombardBTC); + depositCaps = new uint256[](2); + depositCaps[0] = 1e30; + depositCaps[1] = 1e30; + vm.expectRevert("LGE Staking: Tokens array length must equal the Deposit Caps array length."); + lgeStaking = new LGEStaking(hexTrust, address(stETH), address(wstETH), l1Addresses, depositCaps); + + /// LGE Migration + vm.expectRevert("LGE Migration: L1 addresses array length must equal the L2 addresses array length."); + lgeMigration = new LGEMigrationV1( + hexTrust, + address(l1StandardBridge), + address(l1LidoTokensBridge), + address(usdxBridge), + address(lgeStaking), + address(USDC), + address(wstETH), + l1Addresses, + l2Addresses, + restrictedL2Addresses + ); + } + + /// DEPOSIT ERC20 /// + + function testDepositERC20FailureConditions() public prank(alice) { + wBTC.mint(alice, 1e31); + + /// Amount zero + vm.expectRevert("LGE Staking: May not deposit nothing."); + lgeStaking.depositERC20(address(wBTC), 0); + + /// Not allowlisted + vm.expectRevert("LGE Staking: Token must be allowlisted."); + lgeStaking.depositERC20(address(88), 1); + + /// Exceeding deposit caps + wBTC.approve(address(lgeStaking), 1e31); + vm.expectRevert("LGE Staking: deposit amount exceeds deposit cap."); + lgeStaking.depositERC20(address(wBTC), 1e31); + + vm.stopPrank(); + vm.startPrank(hexTrust); + + /// Fee on transfer + TestERC20DecimalsFeeOnTransfer feeOnTransferToken = new TestERC20DecimalsFeeOnTransfer(18); + lgeStaking.setAllowlist(address(feeOnTransferToken), true); + lgeStaking.setDepositCap(address(feeOnTransferToken), 1e30); + feeOnTransferToken.mint(hexTrust, 1e21); + feeOnTransferToken.approve(address(lgeStaking), 1e20); + + vm.expectRevert("LGE Staking: Fee-on-transfer tokens not supported."); + lgeStaking.depositERC20(address(feeOnTransferToken), 1e20); + + /// Migration activated + lgeStaking.setMigrationContract(address(lgeMigration)); + assertEq(lgeStaking.migrationActivated(), true); + vm.stopPrank(); + vm.startPrank(alice); + + vm.expectRevert("LGE Staking: May not deposit once migration has been activated."); + lgeStaking.depositERC20(address(wBTC), 1); + } + + function testDepositERC20SuccessConditions(uint256 _amount) public prank(alice) { + _amount = bound(_amount, 1, 1e30 - 1); + wBTC.mint(alice, 1e31); + wBTC.approve(address(lgeStaking), _amount); + + assertEq(lgeStaking.balance(address(wBTC), alice), 0); + assertEq(lgeStaking.totalDeposited(address(wBTC)), 0); + assertEq(wBTC.balanceOf(address(lgeStaking)), 0); + + vm.expectEmit(true, true, true, true); + emit Deposit(address(wBTC), _amount, alice); + lgeStaking.depositERC20(address(wBTC), _amount); + + assertEq(lgeStaking.balance(address(wBTC), alice), _amount); + assertEq(lgeStaking.totalDeposited(address(wBTC)), _amount); + assertEq(wBTC.balanceOf(address(lgeStaking)), _amount); + } + + /// DEPOSIT ETH /// + + function testDepositETHFailureConditions() public prank(hexTrust) { + vm.deal(hexTrust, 10000 ether); + + /// Amount zero + vm.expectRevert("LGE Staking: May not deposit nothing."); + lgeStaking.depositETH{ value: 0 }(); + + /// Migration activated + lgeStaking.setMigrationContract(address(lgeMigration)); + assertEq(lgeStaking.migrationActivated(), true); + vm.expectRevert("LGE Staking: May not deposit once migration has been activated."); + lgeStaking.depositETH{ value: 1 ether }(); + + lgeStaking.setMigrationContract(address(0)); + + /// Not allowlisted + lgeStaking.setAllowlist(address(wstETH), false); + vm.expectRevert("LGE Staking: Token must be allowlisted."); + lgeStaking.depositETH{ value: 1 ether }(); + + lgeStaking.setAllowlist(address(wstETH), true); + + /// Exceeding deposit caps + lgeStaking.setDepositCap(address(wstETH), 1 ether); + vm.expectRevert("LGE Staking: deposit amount exceeds deposit cap."); + lgeStaking.depositETH{ value: 10 ether }(); + } + + function testDepositETHSuccessConditions(uint256 _amount) public prank(alice) { + _amount = bound(_amount, 1, 1e30 - 1); + vm.deal(alice, 1e31); + + assertEq(lgeStaking.balance(address(wstETH), alice), 0); + assertEq(lgeStaking.totalDeposited(address(wstETH)), 0); + assertEq(wstETH.balanceOf(address(lgeStaking)), 0); + + uint256 predictedWSTETHAmount = wstETH.getWstETHByStETH(_amount); + + vm.expectEmit(true, true, true, true); + emit Deposit(address(wstETH), predictedWSTETHAmount, alice); + lgeStaking.depositETH{ value: _amount }(); + + assertEq(lgeStaking.balance(address(wstETH), alice), predictedWSTETHAmount); + assertEq(lgeStaking.totalDeposited(address(wstETH)), predictedWSTETHAmount); + assertEq(wstETH.balanceOf(address(lgeStaking)), predictedWSTETHAmount); + } + + /// WITHDRAW /// + + function testWithdrawFailureConditions(uint256 _amount) public prank(alice) { + /// Setup + _amount = bound(_amount, 1, 1e30 - 1); + wBTC.mint(alice, 1e31); + wBTC.approve(address(lgeStaking), _amount); + lgeStaking.depositERC20(address(wBTC), _amount); + + /// Amount zero + vm.expectRevert("LGE Staking: may not withdraw nothing."); + lgeStaking.withdraw(address(wBTC), 0); + + /// Insufficient balance + vm.expectRevert("LGE Staking: insufficient deposited balance."); + lgeStaking.withdraw(address(wBTC), _amount + 1); + } + + function testWithdrawSuccessConditions(uint256 _amount0, uint256 _amount1) public prank(alice) { + /// Setup + _amount0 = bound(_amount0, 2, 1e30 - 1); + _amount1 = bound(_amount1, 1, _amount0); + wBTC.mint(alice, 1e31); + wBTC.approve(address(lgeStaking), _amount0); + lgeStaking.depositERC20(address(wBTC), _amount0); + + assertEq(lgeStaking.balance(address(wBTC), alice), _amount0); + assertEq(lgeStaking.totalDeposited(address(wBTC)), _amount0); + assertEq(wBTC.balanceOf(address(lgeStaking)), _amount0); + + vm.expectEmit(true, true, true, true); + emit Withdraw(address(wBTC), _amount1, alice); + lgeStaking.withdraw(address(wBTC), _amount1); + + assertEq(lgeStaking.balance(address(wBTC), alice), _amount0 - _amount1); + assertEq(lgeStaking.totalDeposited(address(wBTC)), _amount0 - _amount1); + assertEq(wBTC.balanceOf(address(lgeStaking)), _amount0 - _amount1); + } + + function testDepositETHAndWithdrawSuccessConditions(uint256 _amount0, uint256 _amount1) public prank(alice) { + /// Setup + _amount0 = bound(_amount0, 2, 1e30 - 1); + uint256 predictedWSTETHAmount = wstETH.getWstETHByStETH(_amount0); + _amount1 = bound(_amount1, 1, predictedWSTETHAmount); + vm.deal(alice, 1e31); + + vm.expectEmit(true, true, true, true); + emit Deposit(address(wstETH), predictedWSTETHAmount, alice); + lgeStaking.depositETH{ value: _amount0 }(); + + assertEq(lgeStaking.balance(address(wstETH), alice), predictedWSTETHAmount); + assertEq(lgeStaking.totalDeposited(address(wstETH)), predictedWSTETHAmount); + assertEq(wstETH.balanceOf(address(lgeStaking)), predictedWSTETHAmount); + + vm.expectEmit(true, true, true, true); + emit Withdraw(address(wstETH), _amount1, alice); + lgeStaking.withdraw(address(wstETH), _amount1); + + assertEq(lgeStaking.balance(address(wstETH), alice), predictedWSTETHAmount - _amount1); + assertEq(lgeStaking.totalDeposited(address(wstETH)), predictedWSTETHAmount - _amount1); + assertEq(wstETH.balanceOf(address(lgeStaking)), predictedWSTETHAmount - _amount1); + } + + /// MIGRATE /// + + function testMigrateFailureConditions(uint256 _amount0) public prank(alice) { + /// Setup + _amount0 = bound(_amount0, 2, 1e30 - 1); + + wBTC.mint(alice, 1e31); + wBTC.approve(address(lgeStaking), _amount0); + lgeStaking.depositERC20(address(wBTC), _amount0); + + assertEq(lgeStaking.balance(address(wBTC), alice), _amount0); + assertEq(lgeStaking.totalDeposited(address(wBTC)), _amount0); + assertEq(wBTC.balanceOf(address(lgeStaking)), _amount0); + + USDC.mint(alice, 1e31); + USDC.approve(address(lgeStaking), _amount0); + lgeStaking.depositERC20(address(USDC), _amount0); + + assertEq(lgeStaking.balance(address(USDC), alice), _amount0); + assertEq(lgeStaking.totalDeposited(address(USDC)), _amount0); + assertEq(USDC.balanceOf(address(lgeStaking)), _amount0); + + /// Only LGE may call + vm.expectRevert("LGE Migration: Only the staking contract can call this function."); + lgeMigration.migrate(alice, l1Addresses, depositCaps); + + address[] memory tokens = new address[](1); + tokens[0] = address(wBTC); + + /// Migration not active + vm.expectRevert("LGE Staking: Migration not active."); + lgeStaking.migrate(alice, tokens); + + /// L2 Destination zero address + vm.stopPrank(); + vm.startPrank(hexTrust); + lgeStaking.setMigrationContract(address(lgeMigration)); + assertEq(lgeStaking.migrationActivated(), true); + vm.stopPrank(); + vm.startPrank(alice); + + vm.expectRevert("LGE Staking: May not send tokens to the zero address."); + lgeStaking.migrate(address(0), tokens); + + /// Tokens length zero + tokens = new address[](0); + + vm.expectRevert("LGE Staking: Must migrate some tokens."); + lgeStaking.migrate(alice, tokens); + + /// No deposits to migrate + tokens = new address[](1); + tokens[0] = address(wSOL); + + vm.expectRevert("LGE Staking: No tokens to migrate."); + lgeStaking.migrate(alice, tokens); + } + + function testMigrateSuccessConditions(uint256 _amount0) public prank(alice) { + /// Setup + _amount0 = bound(_amount0, 2, 1e30 - 1); + wBTC.mint(alice, 1e31); + wBTC.approve(address(lgeStaking), _amount0); + lgeStaking.depositERC20(address(wBTC), _amount0); + + assertEq(lgeStaking.balance(address(wBTC), alice), _amount0); + assertEq(lgeStaking.totalDeposited(address(wBTC)), _amount0); + assertEq(wBTC.balanceOf(address(lgeStaking)), _amount0); + + /// Migrate + vm.stopPrank(); + vm.startPrank(hexTrust); + lgeStaking.setMigrationContract(address(lgeMigration)); + assertEq(lgeStaking.migrationActivated(), true); + vm.stopPrank(); + vm.startPrank(alice); + + address[] memory tokens = new address[](1); + tokens[0] = address(wBTC); + uint256[] memory amounts = new uint256[](1); + amounts[0] = _amount0; + + vm.expectEmit(true, true, true, true); + emit TokensMigrated(alice, alice, tokens, amounts); + lgeStaking.migrate(alice, tokens); + + assertEq(lgeStaking.balance(address(wBTC), alice), 0); + assertEq(lgeStaking.totalDeposited(address(wBTC)), 0); + assertEq(wBTC.balanceOf(address(lgeStaking)), 0); + } + + function testRecoverTokens() public prank(alice) { + /// Setup + uint256 _amount0 = 100e18; + wBTC.mint(alice, _amount0); + wBTC.transfer(address(lgeMigration), _amount0); + assertEq(wBTC.balanceOf(address(lgeMigration)), _amount0); + + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeMigration.recoverTokens(address(wBTC), _amount0, alice); + + vm.stopPrank(); + vm.startPrank(hexTrust); + + /// Recover + lgeMigration.recoverTokens(address(wBTC), _amount0, alice); + assertEq(wBTC.balanceOf(address(lgeMigration)), 0); + } + + function testSetGasLimit() public prank(alice) { + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeMigration.setGasLimit(address(wstETH), 1e6); + + assertEq(lgeMigration.gasLimits(address(wstETH)), 21000); + + vm.stopPrank(); + vm.startPrank(hexTrust); + + /// Set new gas limit + lgeMigration.setGasLimit(address(wstETH), 1e6); + + assertEq(lgeMigration.gasLimits(address(wstETH)), 1e6); + } + + /// OWNER /// + + function testSetAllowlist() public { + TestERC20Decimals USDD = new TestERC20Decimals(18); + + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeStaking.setAllowlist(address(USDD), true); + + /// Owner allowed to set new coin + vm.startPrank(hexTrust); + + /// Add USDD + vm.expectEmit(true, true, true, true); + emit AllowlistSet(address(USDD), true); + lgeStaking.setAllowlist(address(USDD), true); + + /// Remove USDC + vm.expectEmit(true, true, true, true); + emit AllowlistSet(address(USDC), false); + lgeStaking.setAllowlist(address(USDC), false); + + vm.stopPrank(); + + assertEq(lgeStaking.allowlisted(address(USDD)), true); + assertEq(lgeStaking.allowlisted(address(USDC)), false); + } + + function testSetDepositCap(uint256 _newCap) public { + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeStaking.setDepositCap(address(USDC), _newCap); + + assertEq(lgeStaking.depositCap(address(USDC)), 1e30); + + /// Owner allowed + vm.startPrank(hexTrust); + + vm.expectEmit(true, true, true, true); + emit DepositCapSet(address(USDC), _newCap); + lgeStaking.setDepositCap(address(USDC), _newCap); + + vm.stopPrank(); + + assertEq(lgeStaking.depositCap(address(USDC)), _newCap); + } + + function testSetPaused() public { + vm.deal(hexTrust, 10000 ether); + + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeStaking.setPaused(true); + + assertEq(lgeStaking.paused(), false); + + /// Owner allowed + vm.startPrank(hexTrust); + + vm.expectEmit(true, true, true, true); + emit Paused(hexTrust); + lgeStaking.setPaused(true); + + assertEq(lgeStaking.paused(), true); + + /// External functions paused + vm.expectRevert("Pausable: paused"); + lgeStaking.depositERC20(address(wBTC), 1e18); + + vm.expectRevert("Pausable: paused"); + lgeStaking.depositETH{ value: 1e18 }(); + + vm.expectRevert("Pausable: paused"); + lgeStaking.withdraw(address(wBTC), 1e18); + + address[] memory tokensArray; + vm.expectRevert("Pausable: paused"); + lgeStaking.migrate(alice, tokensArray); + + vm.expectEmit(true, true, true, true); + emit Unpaused(hexTrust); + lgeStaking.setPaused(false); + + assertEq(lgeStaking.paused(), false); + + vm.stopPrank(); + } + + function testSetMigrationContract() public { + address newMigrationContract = address(88); + + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + lgeStaking.setMigrationContract(newMigrationContract); + + assertEq(address(lgeStaking.lgeMigration()), address(0)); + assertEq(lgeStaking.migrationActivated(), false); + + vm.startPrank(hexTrust); + + vm.expectEmit(true, true, true, true); + emit MigrationContractSet(newMigrationContract); + lgeStaking.setMigrationContract(newMigrationContract); + + assertEq(address(lgeStaking.lgeMigration()), newMigrationContract); + assertEq(lgeStaking.migrationActivated(), true); + + vm.stopPrank(); + } +} diff --git a/packages/contracts-bedrock/test/L1/USDXBridge.t.sol b/packages/contracts-bedrock/test/L1/USDXBridge.t.sol index 4c756e4bc7d..74e8fbf9726 100644 --- a/packages/contracts-bedrock/test/L1/USDXBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/USDXBridge.t.sol @@ -26,6 +26,7 @@ contract USDXBridgeTest is CommonTest { event WithdrawCoins(address indexed _coin, uint256 _amount, address indexed _to); event AllowlistSet(address indexed _coin, bool _set); event DepositCapSet(address indexed _coin, uint256 _newDepositCap); + event GasLimitSet(uint64 _newGasLimit); function setUp() public override { /// Set up environment @@ -73,7 +74,7 @@ contract USDXBridgeTest is CommonTest { uint256[] memory depositCaps = new uint256[](2); depositCaps[0] = 1e30; depositCaps[1] = 1e30; - vm.expectRevert("USDXBridge: Stablecoins array length must equal the Deposit Caps array length."); + vm.expectRevert("USDX Bridge: Stablecoins array length must equal the Deposit Caps array length."); usdxBridge = new USDXBridge(hexTrust, optimismPortal, systemConfig, stablecoins, depositCaps); } @@ -88,6 +89,7 @@ contract USDXBridgeTest is CommonTest { assertEq(address(usdxBridge.usdx()), address(usdx)); assertEq(address(usdxBridge.portal()), address(optimismPortal)); assertEq(address(usdxBridge.config()), address(systemConfig)); + assertEq(usdxBridge.gasLimit(), 21000); assertEq(usdx.allowance(address(usdxBridge), address(optimismPortal)), 0); assertEq(usdxBridge.allowlisted(address(usdc)), true); assertEq(usdxBridge.allowlisted(address(usdt)), true); @@ -132,16 +134,16 @@ contract USDXBridgeTest is CommonTest { /// Non-accepted stablecoin/ERC20 TestERC20Decimals usde = new TestERC20Decimals(18); - vm.expectRevert("USDXBridge: Stablecoin not accepted."); + vm.expectRevert("USDX Bridge: Stablecoin not accepted."); usdxBridge.bridge(address(usde), _amount, alice); /// Deposit zero - vm.expectRevert("USDXBridge: May not bridge nothing."); + vm.expectRevert("USDX Bridge: May not bridge nothing."); usdxBridge.bridge(address(dai), 0, alice); /// Deposit Cap exceeded uint256 excess = usdxBridge.depositCap(address(dai)) + 1; - vm.expectRevert("USDXBridge: Bridge amount exceeds deposit cap."); + vm.expectRevert("USDX Bridge: Bridge amount exceeds deposit cap."); usdxBridge.bridge(address(dai), excess, alice); } @@ -313,6 +315,21 @@ contract USDXBridgeTest is CommonTest { assertEq(usdxBridge.depositCap(address(usdc)), _newCap); } + function testSetGasLimit(uint64 _newGasLimit) public { + /// Non-owner revert + vm.expectRevert("Ownable: caller is not the owner"); + usdxBridge.setGasLimit(_newGasLimit); + assertEq(usdxBridge.gasLimit(), 21000); + + /// Owner allowed + vm.startPrank(hexTrust); + vm.expectEmit(true, true, true, true); + emit GasLimitSet(_newGasLimit); + usdxBridge.setGasLimit(_newGasLimit); + vm.stopPrank(); + assertEq(usdxBridge.gasLimit(), _newGasLimit); + } + function testWithdrawERC20(uint256 _amount) public { /// Send some tokens directly to the contract dai.mint(address(usdxBridge), _amount); diff --git a/packages/contracts-bedrock/test/L2/OzUSD.t.sol b/packages/contracts-bedrock/test/L2/OzUSD.t.sol index c9440cf9549..4887d02106a 100644 --- a/packages/contracts-bedrock/test/L2/OzUSD.t.sol +++ b/packages/contracts-bedrock/test/L2/OzUSD.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.15; import { console2 as console } from "forge-std/console2.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { OzUSD } from "src/L2/OzUSD.sol"; -import { OzUSDDeploy, TransparentUpgradeableProxy } from "scripts/ozean/OzUSDDeploy.s.sol"; +import { OzUSDDeploy } from "scripts/ozean/OzUSDDeploy.s.sol"; /// @dev forge test --match-contract OzUSDTest -vvv contract OzUSDTest is CommonTest { @@ -12,6 +12,10 @@ contract OzUSDTest is CommonTest { OzUSD public implementation; OzUSD public ozUSD; + event TransferShares(address indexed from, address indexed to, uint256 sharesValue); + event SharesBurnt(address indexed account, uint256 preRebaseTokenAmount, uint256 postRebaseTokenAmount, uint256 sharesAmount); + event YieldDistributed(uint256 _previousTotalBalance, uint256 _newTotalBalance); + function setUp() public override { alice = makeAddr("alice"); bob = makeAddr("bob"); @@ -22,9 +26,9 @@ contract OzUSDTest is CommonTest { /// Deploy OzUSD OzUSDDeploy deployScript = new OzUSDDeploy(); + deployScript.setUp(admin); deployScript.run(); - implementation = deployScript.implementation(); - ozUSD = OzUSD(payable(deployScript.proxy())); + ozUSD = deployScript.ozUSD(); } /// SETUP /// @@ -37,22 +41,75 @@ contract OzUSDTest is CommonTest { assertEq(ozUSD.decimals(), 18); } + function testDeployRevertConditions() public { + /// Deploy with less than 1 USDX + uint256 initialSharesAmount = 1e18; + vm.expectRevert("OzUSD: Must deploy with at least one USDX."); + new OzUSD{value: initialSharesAmount - 1}(admin, initialSharesAmount - 1); + + /// Wrong value + vm.expectRevert("OzUSD: Incorrect value."); + new OzUSD{value: initialSharesAmount}(admin, initialSharesAmount - 1); + } + /// REBASE /// + function testMintRevertConditions() public prank(alice) { + /// Amount zero + vm.expectRevert("OzUSD: Amount zero."); + ozUSD.mintOzUSD{ value: 1e18 }(alice, 0); + + /// Insufficient amount + vm.expectRevert("OzUSD: Insufficient USDX transfer."); + ozUSD.mintOzUSD{ value: 1e18 }(alice, 1e18 + 1); + + /// Mint to zero address + vm.expectRevert("OzUSD: Mint to zero address."); + ozUSD.mintOzUSD{ value: 1e18 }(address(0), 1e18); + } + + function testRedeemOzUSDRevertConditions() public prank(alice) { + uint256 _amountA = 100 ether; + + assertEq(address(ozUSD).balance, 1e18); + assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); + + ozUSD.mintOzUSD{ value: _amountA }(alice, _amountA); + + assertEq(address(ozUSD).balance, 1e18 + _amountA); + assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); + + /// Amount zero + vm.expectRevert("OzUSD: Amount zero."); + ozUSD.redeemOzUSD(alice, 0); + + /// Burn more than allowance + vm.expectRevert("OzUSD: Balance exceeded."); + ozUSD.redeemOzUSD(alice, 1e30); + + /// Allowance + vm.stopPrank(); + vm.startPrank(bob); + vm.expectRevert("OzUSD: Allowance exceeded."); + ozUSD.redeemOzUSD(alice, 1e30); + } + function testRebase(uint256 sharesAmount) public prank(alice) { - sharesAmount = bound(sharesAmount, 1, 1e20); + sharesAmount = bound(sharesAmount, 1e18 + 1, 1e20); assertEq(address(ozUSD).balance, 1e18); assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), sharesAmount); - (bool s,) = address(ozUSD).call{ value: sharesAmount }(""); - assert(s); + vm.expectEmit(true, true, true, true); + emit YieldDistributed(1e18, 1e18 + sharesAmount); + (bool s, ) = address(ozUSD).call{value: sharesAmount}(""); + require(s); assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), (sharesAmount * address(ozUSD).balance) / 1e18); } function testMintAndRebase(uint256 _amountA, uint256 _amountB) public prank(alice) { - _amountA = bound(_amountA, 1, 1e21); - _amountB = bound(_amountB, 1, 1e21); + _amountA = bound(_amountA, 1e18 + 1, 1e21); + _amountB = bound(_amountB, 1e18 + 1, 1e21); assertEq(address(ozUSD).balance, 1e18); assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); @@ -62,8 +119,10 @@ contract OzUSDTest is CommonTest { assertEq(address(ozUSD).balance, 1e18 + _amountA); assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); - (bool s,) = address(ozUSD).call{ value: _amountB }(""); - assert(s); + vm.expectEmit(true, true, true, true); + emit YieldDistributed(1e18 + _amountA, 1e18 + _amountA + _amountB); + (bool s, ) = address(ozUSD).call{value: _amountB}(""); + require(s); assertEq(address(ozUSD).balance, 1e18 + _amountA + _amountB); assertEq(ozUSD.balanceOf(alice), ozUSD.getPooledUSDXByShares(_amountA)); @@ -90,8 +149,8 @@ contract OzUSDTest is CommonTest { } function testMintRebaseAndRedeem(uint256 _amountA, uint256 _amountB) public prank(alice) { - _amountA = bound(_amountA, 1, 1e21); - _amountB = bound(_amountB, 1, 1e21); + _amountA = bound(_amountA, 1e18 + 1, 1e21); + _amountB = bound(_amountB, 1e18 + 1, 1e21); assertEq(address(ozUSD).balance, 1e18); assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); @@ -102,8 +161,10 @@ contract OzUSDTest is CommonTest { assertEq(ozUSD.balanceOf(alice), _amountA); assertEq(ozUSD.getPooledUSDXByShares(_amountA), _amountA); - (bool s,) = address(ozUSD).call{ value: _amountB }(""); - assert(s); + vm.expectEmit(true, true, true, true); + emit YieldDistributed(1e18 + _amountA, 1e18 + _amountA + _amountB); + (bool s, ) = address(ozUSD).call{value: _amountB}(""); + require(s); uint256 predictedAliceAmount = (_amountA * (1e18 + _amountA + _amountB)) / (1e18 + _amountA); @@ -115,8 +176,6 @@ contract OzUSDTest is CommonTest { ozUSD.redeemOzUSD(alice, predictedAliceAmount); assertEq(address(ozUSD).balance, (1e18 + _amountA + _amountB) - predictedAliceAmount); - /// @dev precision loss here - ///assertEq(ozUSD.getPooledUSDXByShares(_amountA), predictedFinalAmount); } function testMintRebaseAndRedeem() public prank(alice) { @@ -131,22 +190,37 @@ contract OzUSDTest is CommonTest { assertEq(ozUSD.balanceOf(alice), 1e18); assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), 1e18); - (bool s,) = address(ozUSD).call{ value: 1e18 }(""); - assert(s); + vm.expectEmit(true, true, true, true); + emit YieldDistributed(2e18, 4e18); + (bool s, ) = address(ozUSD).call{value: 2e18}(""); + require(s); - assertEq(address(ozUSD).balance, 3e18); - assertEq(ozUSD.balanceOf(alice), 1.5e18); - assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), 1.5e18); + assertEq(address(ozUSD).balance, 4e18); + assertEq(ozUSD.balanceOf(alice), 2e18); + assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), 2e18); - ozUSD.approve(alice, 1.5e18); - ozUSD.redeemOzUSD(alice, 1.5e18); + ozUSD.approve(alice, 2e18); + ozUSD.redeemOzUSD(alice, 2e18); - assertEq(address(ozUSD).balance, 1.5e18); - assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), 1.5e18); + assertEq(address(ozUSD).balance, 2e18); + assertEq(ozUSD.getPooledUSDXByShares(sharesAmount), 2e18); } /// ERC20 /// + function testApproveRevertConditions() public prank(address(0)) { + /// Approve from zero address + vm.expectRevert("OzUSD: Approve from zero address."); + ozUSD.approve(alice, 1e18); + + vm.stopPrank(); + vm.startPrank(alice); + + /// Approve to zero address + vm.expectRevert("OzUSD: Approve to zero address."); + ozUSD.approve(address(0), 1e18); + } + function testApproveAndTransferFrom() public prank(alice) { uint256 sharesAmount = 1e18; @@ -177,23 +251,61 @@ contract OzUSDTest is CommonTest { ozUSD.mintOzUSD{ value: 1e18 }(alice, 1e18); assertEq(ozUSD.balanceOf(alice), 1e18); - // Increase bob's allowance + // Increase Bob's allowance ozUSD.increaseAllowance(bob, 0.5e18); assertEq(ozUSD.allowance(alice, bob), 0.5e18); - // Decrease bob's allowance + // Decrease Bob's allowance ozUSD.decreaseAllowance(bob, 0.2e18); assertEq(ozUSD.allowance(alice, bob), 0.3e18); + + /// Decrease Bob's allowance revert + vm.expectRevert("OzUSD: Allowance below value."); + ozUSD.decreaseAllowance(bob, 0.4e18); + } + + function testTransferSharesRevertConditions() public prank(address(0)) { + /// Transfer from zero address + vm.expectRevert("OzUSD: Transfer from zero address."); + ozUSD.transferShares(alice, 1e18); + + /// Transfer to zero address + vm.stopPrank(); + vm.startPrank(alice); + + vm.expectRevert("OzUSD: Transfer to zero address."); + ozUSD.transferShares(address(0), 1e18); + + /// Transfer to contract. + vm.expectRevert("OzUSD: Transfer to this contract."); + ozUSD.transferShares(address(ozUSD), 1e18); } function testTransferShares() public prank(alice) { // Mint ozUSD ozUSD.mintOzUSD{ value: 1e18 }(alice, 1e18); + assertEq(ozUSD.sharesOf(alice), 1e18); // Transfer shares from alice to bob uint256 sharesToTransfer = 0.5e18; uint256 tokensTransferred = ozUSD.transferShares(bob, sharesToTransfer); + // Check balances after the transfer + assertEq(ozUSD.balanceOf(alice), 0.5e18); + assertEq(ozUSD.balanceOf(bob), tokensTransferred); + assertEq(ozUSD.sharesOf(alice), 0.5e18); + assertEq(ozUSD.sharesOf(bob), 0.5e18); + } + + function testTransferSharesFrom() public prank(alice) { + // Mint ozUSD + ozUSD.mintOzUSD{ value: 1e18 }(alice, 1e18); + + // Transfer shares from alice to bob + uint256 sharesToTransfer = 0.5e18; + ozUSD.approve(alice, ~uint256(0)); + uint256 tokensTransferred = ozUSD.transferSharesFrom(alice, bob, sharesToTransfer); + // Check balances after the transfer assertEq(ozUSD.balanceOf(alice), 0.5e18); assertEq(ozUSD.balanceOf(bob), tokensTransferred); @@ -204,7 +316,7 @@ contract OzUSDTest is CommonTest { ozUSD.mintOzUSD{ value: 1e18 }(alice, 1e18); // Attempt to transfer more than alice's balance - vm.expectRevert("OzUSD: BALANCE_EXCEEDED"); + vm.expectRevert("OzUSD: Balance exceeded."); ozUSD.transfer(bob, 2e18); // Transfer amount exceeds balance } @@ -234,36 +346,7 @@ contract OzUSDTest is CommonTest { // Bob tries to transfer more than allowed, should fail vm.stopPrank(); vm.startPrank(bob); - vm.expectRevert("OzUSD: ALLOWANCE_EXCEEDED"); + vm.expectRevert("OzUSD: Allowance exceeded."); ozUSD.transferFrom(alice, bob, 1e18); } - - /// PROXY /// - - /// @dev Can only be called by admin, otherwise delegatecalls to impl - function testAdmin() public prank(admin) { - assertEq(TransparentUpgradeableProxy(payable(ozUSD)).admin(), admin); - } - - function testProxyInitialize() public { - vm.expectRevert("Initializable: contract is already initialized"); - implementation.initialize{ value: 1e18 }(1e18); - - assertEq(address(implementation).balance, 0); - - vm.expectRevert("Initializable: contract is already initialized"); - ozUSD.initialize{ value: 1e18 }(1e18); - - assertEq(address(ozUSD).balance, 1e18); - } - - function testUpgradeImplementation() public prank(admin) { - OzUSD newImplementation = new OzUSD(); - - assertEq(TransparentUpgradeableProxy(payable(ozUSD)).implementation(), address(implementation)); - - TransparentUpgradeableProxy(payable(ozUSD)).upgradeToAndCall(address(newImplementation), ""); - - assertEq(TransparentUpgradeableProxy(payable(ozUSD)).implementation(), address(newImplementation)); - } } diff --git a/packages/contracts-bedrock/test/L2/WozUSD.t.sol b/packages/contracts-bedrock/test/L2/WozUSD.t.sol index a0f74d4f295..61dbd33a432 100644 --- a/packages/contracts-bedrock/test/L2/WozUSD.t.sol +++ b/packages/contracts-bedrock/test/L2/WozUSD.t.sol @@ -10,19 +10,23 @@ import { WozUSDDeploy } from "scripts/ozean/WozUSDDeploy.s.sol"; /// @dev forge test --match-contract WozUSDTest -vvv contract WozUSDTest is CommonTest { + address public admin; OzUSD public ozUSD; WozUSD public wozUSD; function setUp() public override { alice = makeAddr("alice"); bob = makeAddr("bob"); + admin = makeAddr("admin"); vm.deal(alice, 10000 ether); vm.deal(bob, 10000 ether); + vm.deal(admin, 10000 ether); /// Deploy OzUSD OzUSDDeploy ozDeployScript = new OzUSDDeploy(); + ozDeployScript.setUp(admin); ozDeployScript.run(); - ozUSD = OzUSD(payable(ozDeployScript.proxy())); + ozUSD = OzUSD(payable(ozDeployScript.ozUSD())); /// Deploy WozUSD WozUSDDeploy wozDeployScript = new WozUSDDeploy(); @@ -128,23 +132,6 @@ contract WozUSDTest is CommonTest { assertEq(ozUSD.balanceOf(alice), 1.5e18); } - function testWrapAndRebaseSmallAmount() public prank(alice) { - uint256 sharesAmount = 0.001e18; - ozUSD.mintOzUSD{ value: sharesAmount }(alice, sharesAmount); - - /// Wrap - ozUSD.approve(address(wozUSD), ~uint256(0)); - wozUSD.wrap(sharesAmount); - - /// Mock rebase - (bool s,) = address(ozUSD).call{ value: sharesAmount }(""); - assert(s); - - /// Unwrap - wozUSD.unwrap(sharesAmount); - assertGt(ozUSD.balanceOf(alice), sharesAmount); - } - function testMultipleWrapUnwrap() public prank(alice) { uint256 sharesAmount = 1e18; diff --git a/packages/contracts-bedrock/test/mocks/TestERC20.sol b/packages/contracts-bedrock/test/mocks/TestERC20.sol index d0d32f5678b..57a76b90985 100644 --- a/packages/contracts-bedrock/test/mocks/TestERC20.sol +++ b/packages/contracts-bedrock/test/mocks/TestERC20.sol @@ -18,3 +18,36 @@ contract TestERC20Decimals is ERC20 { _mint(to, value); } } + +contract TestERC20DecimalsFeeOnTransfer is ERC20 { + constructor(uint8 _decimals) ERC20("TEST", "TST", _decimals) {} + + function mint(address to, uint256 value) public { + _mint(to, value); + } + + function transfer(address to, uint256 amount) public override returns (bool) { + balanceOf[msg.sender] -= amount; + unchecked { + balanceOf[to] += (amount - 1); + } + emit Transfer(msg.sender, to, amount - 1); + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public override returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; + balanceOf[from] -= amount; + unchecked { + balanceOf[to] += (amount - 1); + } + emit Transfer(from, to, amount - 1); + return true; + } +} + diff --git a/packages/contracts-bedrock/test/mocks/TestLido.sol b/packages/contracts-bedrock/test/mocks/TestLido.sol new file mode 100644 index 00000000000..13750c6659c --- /dev/null +++ b/packages/contracts-bedrock/test/mocks/TestLido.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ERC20 } from "@rari-capital/solmate/src/tokens/ERC20.sol"; + +contract TestStETH is ERC20 { + constructor() ERC20("staked ETH", "STETH", 18) { } + + function submit(address _referral) external payable returns (uint256) { + _referral; + _mint(msg.sender, msg.value); + return msg.value; + } +} + +contract TestWstETH is ERC20 { + address public immutable stETH; + + constructor(address _stETH) ERC20("Wrapped Staked ETH", "WSTETH", 18) { + stETH = _stETH; + } + + function wrap(uint256 _stETHAmount) external returns (uint256) { + ERC20(stETH).transferFrom(msg.sender, address(this), _stETHAmount); + uint256 wstETHAmount = getWstETHByStETH(_stETHAmount); + _mint(msg.sender, wstETHAmount); + return wstETHAmount; + } + + /// @dev Hard code stETH/wstETH factor of 0.9 + function getWstETHByStETH(uint256 _stETHAmount) public pure returns (uint256) { + return (_stETHAmount * 9) / 10; + } +}