Transaction Hash:
Block:
21733484 at Jan-29-2025 11:31:23 PM +UTC
Transaction Fee:
0.000109943483039742 ETH
$0.27
Gas Used:
40,359 Gas / 2.724137938 Gwei
Emitted Events:
564 |
TransparentUpgradeableProxy.0x8b65b80ac62fde507cb8196bad6c93c114c2babc6ac846aae39ed6943ad36c49( 0x8b65b80ac62fde507cb8196bad6c93c114c2babc6ac846aae39ed6943ad36c49, 0x0000000000000000000000007a93fc67e62b5ead973ae530827823ae9a093d33, 0x0000000000000000000000002260fac5e5542a773aa44fbcfedf7c193bc2c599, 0x000000000000000000000000000000000000000000000000000000000000123a, 000000000000000000000000000000000000000000000000000000006902a3cb, 000000000000000000000000000000000000000000000000000000000167e980 )
|
Account State Difference:
Address | Before | After | State Difference | ||
---|---|---|---|---|---|
0x7A93FC67...e9A093D33 |
0.002123688729538884 Eth
Nonce: 16
|
0.002013745246499142 Eth
Nonce: 17
| 0.000109943483039742 | ||
0x95222290...5CC4BAfe5
Miner
| (beaverbuild) | 11.229574506050666216 Eth | 11.229576402923666216 Eth | 0.000001896873 | |
0xAB13B8ee...1fD5B8A39 | (Mezo: Portal Proxy) |
Execution Trace
TransparentUpgradeableProxy.f5e8d327( )
-
Portal.lock( token=0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, depositId=4666, lockPeriod=23587200 )
lock[Portal (ln:1726)]
DepositNotFound[Portal (ln:1735)]
InsufficientTokenAbility[Portal (ln:1739)]
_calculateUnlockTime[Portal (ln:1742)]
_normalizeLockPeriod[Portal (ln:2066)]
LockPeriodOutOfRange[Portal (ln:2072)]
Locked[Portal (ln:2079)]
LockPeriodTooShort[Portal (ln:1749)]
File 1 of 2: TransparentUpgradeableProxy
File 2 of 2: Portal
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol) pragma solidity ^0.8.20; import {Context} from "../utils/Context.sol"; /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * The initial owner is set to the address provided by the deployer. This can * later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract Ownable is Context { address private _owner; /** * @dev The caller account is not authorized to perform an operation. */ error OwnableUnauthorizedAccount(address account); /** * @dev The owner is not a valid owner account. (eg. `address(0)`) */ error OwnableInvalidOwner(address owner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ constructor(address initialOwner) { if (initialOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(initialOwner); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { return _owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { if (owner() != _msgSender()) { revert OwnableUnauthorizedAccount(_msgSender()); } } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { if (newOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { address oldOwner = _owner; _owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1967.sol) pragma solidity ^0.8.20; /** * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. */ interface IERC1967 { /** * @dev Emitted when the implementation is upgraded. */ event Upgraded(address indexed implementation); /** * @dev Emitted when the admin account has changed. */ event AdminChanged(address previousAdmin, address newAdmin); /** * @dev Emitted when the beacon is changed. */ event BeaconUpgraded(address indexed beacon); } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/BeaconProxy.sol) pragma solidity ^0.8.20; import {IBeacon} from "./IBeacon.sol"; import {Proxy} from "../Proxy.sol"; import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol"; /** * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. * * The beacon address can only be set once during construction, and cannot be changed afterwards. It is stored in an * immutable variable to avoid unnecessary storage reads, and also in the beacon storage slot specified by * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] so that it can be accessed externally. * * CAUTION: Since the beacon address can never be changed, you must ensure that you either control the beacon, or trust * the beacon to not upgrade the implementation maliciously. * * IMPORTANT: Do not use the implementation logic to modify the beacon storage slot. Doing so would leave the proxy in * an inconsistent state where the beacon storage slot does not match the beacon address. */ contract BeaconProxy is Proxy { // An immutable address for the beacon to avoid unnecessary SLOADs before each delegate call. address private immutable _beacon; /** * @dev Initializes the proxy with `beacon`. * * If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This * will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity * constructor. * * Requirements: * * - `beacon` must be a contract with the interface {IBeacon}. * - If `data` is empty, `msg.value` must be zero. */ constructor(address beacon, bytes memory data) payable { ERC1967Utils.upgradeBeaconToAndCall(beacon, data); _beacon = beacon; } /** * @dev Returns the current implementation address of the associated beacon. */ function _implementation() internal view virtual override returns (address) { return IBeacon(_getBeacon()).implementation(); } /** * @dev Returns the beacon. */ function _getBeacon() internal view virtual returns (address) { return _beacon; } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/IBeacon.sol) pragma solidity ^0.8.20; /** * @dev This is the interface that {BeaconProxy} expects of its beacon. */ interface IBeacon { /** * @dev Must return an address that can be used as a delegate call target. * * {UpgradeableBeacon} will check that this address is a contract. */ function implementation() external view returns (address); } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/UpgradeableBeacon.sol) pragma solidity ^0.8.20; import {IBeacon} from "./IBeacon.sol"; import {Ownable} from "../../access/Ownable.sol"; /** * @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their * implementation contract, which is where they will delegate all function calls. * * An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon. */ contract UpgradeableBeacon is IBeacon, Ownable { address private _implementation; /** * @dev The `implementation` of the beacon is invalid. */ error BeaconInvalidImplementation(address implementation); /** * @dev Emitted when the implementation returned by the beacon is changed. */ event Upgraded(address indexed implementation); /** * @dev Sets the address of the initial implementation, and the initial owner who can upgrade the beacon. */ constructor(address implementation_, address initialOwner) Ownable(initialOwner) { _setImplementation(implementation_); } /** * @dev Returns the current implementation address. */ function implementation() public view virtual returns (address) { return _implementation; } /** * @dev Upgrades the beacon to a new implementation. * * Emits an {Upgraded} event. * * Requirements: * * - msg.sender must be the owner of the contract. * - `newImplementation` must be a contract. */ function upgradeTo(address newImplementation) public virtual onlyOwner { _setImplementation(newImplementation); } /** * @dev Sets the implementation contract address for this beacon * * Requirements: * * - `newImplementation` must be a contract. */ function _setImplementation(address newImplementation) private { if (newImplementation.code.length == 0) { revert BeaconInvalidImplementation(newImplementation); } _implementation = newImplementation; emit Upgraded(newImplementation); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/ERC1967/ERC1967Proxy.sol) pragma solidity ^0.8.20; import {Proxy} from "../Proxy.sol"; import {ERC1967Utils} from "./ERC1967Utils.sol"; /** * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an * implementation address that can be changed. This address is stored in storage in the location specified by * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the * implementation behind the proxy. */ contract ERC1967Proxy is Proxy { /** * @dev Initializes the upgradeable proxy with an initial implementation specified by `implementation`. * * If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an * encoded function call, and allows initializing the storage of the proxy like a Solidity constructor. * * Requirements: * * - If `data` is empty, `msg.value` must be zero. */ constructor(address implementation, bytes memory _data) payable { ERC1967Utils.upgradeToAndCall(implementation, _data); } /** * @dev Returns the current implementation address. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function _implementation() internal view virtual override returns (address) { return ERC1967Utils.getImplementation(); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/ERC1967/ERC1967Utils.sol) pragma solidity ^0.8.20; import {IBeacon} from "../beacon/IBeacon.sol"; import {Address} from "../../utils/Address.sol"; import {StorageSlot} from "../../utils/StorageSlot.sol"; /** * @dev This abstract contract provides getters and event emitting update functions for * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. */ library ERC1967Utils { // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. // This will be fixed in Solidity 0.8.21. At that point we should remove these events. /** * @dev Emitted when the implementation is upgraded. */ event Upgraded(address indexed implementation); /** * @dev Emitted when the admin account has changed. */ event AdminChanged(address previousAdmin, address newAdmin); /** * @dev Emitted when the beacon is changed. */ event BeaconUpgraded(address indexed beacon); /** * @dev Storage slot with the address of the current implementation. * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1. */ // solhint-disable-next-line private-vars-leading-underscore bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /** * @dev The `implementation` of the proxy is invalid. */ error ERC1967InvalidImplementation(address implementation); /** * @dev The `admin` of the proxy is invalid. */ error ERC1967InvalidAdmin(address admin); /** * @dev The `beacon` of the proxy is invalid. */ error ERC1967InvalidBeacon(address beacon); /** * @dev An upgrade function sees `msg.value > 0` that may be lost. */ error ERC1967NonPayable(); /** * @dev Returns the current implementation address. */ function getImplementation() internal view returns (address) { return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; } /** * @dev Stores a new address in the EIP1967 implementation slot. */ function _setImplementation(address newImplementation) private { if (newImplementation.code.length == 0) { revert ERC1967InvalidImplementation(newImplementation); } StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; } /** * @dev Performs implementation upgrade with additional setup call if data is nonempty. * This function is payable only if the setup call is performed, otherwise `msg.value` is rejected * to avoid stuck value in the contract. * * Emits an {IERC1967-Upgraded} event. */ function upgradeToAndCall(address newImplementation, bytes memory data) internal { _setImplementation(newImplementation); emit Upgraded(newImplementation); if (data.length > 0) { Address.functionDelegateCall(newImplementation, data); } else { _checkNonPayable(); } } /** * @dev Storage slot with the admin of the contract. * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1. */ // solhint-disable-next-line private-vars-leading-underscore bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; /** * @dev Returns the current admin. * * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using * the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ function getAdmin() internal view returns (address) { return StorageSlot.getAddressSlot(ADMIN_SLOT).value; } /** * @dev Stores a new address in the EIP1967 admin slot. */ function _setAdmin(address newAdmin) private { if (newAdmin == address(0)) { revert ERC1967InvalidAdmin(address(0)); } StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin; } /** * @dev Changes the admin of the proxy. * * Emits an {IERC1967-AdminChanged} event. */ function changeAdmin(address newAdmin) internal { emit AdminChanged(getAdmin(), newAdmin); _setAdmin(newAdmin); } /** * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. * This is the keccak-256 hash of "eip1967.proxy.beacon" subtracted by 1. */ // solhint-disable-next-line private-vars-leading-underscore bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; /** * @dev Returns the current beacon. */ function getBeacon() internal view returns (address) { return StorageSlot.getAddressSlot(BEACON_SLOT).value; } /** * @dev Stores a new beacon in the EIP1967 beacon slot. */ function _setBeacon(address newBeacon) private { if (newBeacon.code.length == 0) { revert ERC1967InvalidBeacon(newBeacon); } StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon; address beaconImplementation = IBeacon(newBeacon).implementation(); if (beaconImplementation.code.length == 0) { revert ERC1967InvalidImplementation(beaconImplementation); } } /** * @dev Change the beacon and trigger a setup call if data is nonempty. * This function is payable only if the setup call is performed, otherwise `msg.value` is rejected * to avoid stuck value in the contract. * * Emits an {IERC1967-BeaconUpgraded} event. * * CAUTION: Invoking this function has no effect on an instance of {BeaconProxy} since v5, since * it uses an immutable beacon without looking at the value of the ERC-1967 beacon slot for * efficiency. */ function upgradeBeaconToAndCall(address newBeacon, bytes memory data) internal { _setBeacon(newBeacon); emit BeaconUpgraded(newBeacon); if (data.length > 0) { Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); } else { _checkNonPayable(); } } /** * @dev Reverts if `msg.value` is not zero. It can be used to avoid `msg.value` stuck in the contract * if an upgrade doesn't perform an initialization call. */ function _checkNonPayable() private { if (msg.value > 0) { revert ERC1967NonPayable(); } } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/Proxy.sol) pragma solidity ^0.8.20; /** * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to * be specified by overriding the virtual {_implementation} function. * * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a * different contract through the {_delegate} function. * * The success and return data of the delegated call will be returned back to the caller of the proxy. */ abstract contract Proxy { /** * @dev Delegates the current call to `implementation`. * * This function does not return to its internal call site, it will return directly to the external caller. */ function _delegate(address implementation) internal virtual { assembly { // Copy msg.data. We take full control of memory in this inline assembly // block because it will not return to Solidity code. We overwrite the // Solidity scratch pad at memory position 0. calldatacopy(0, 0, calldatasize()) // Call the implementation. // out and outsize are 0 because we don't know the size yet. let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) // Copy the returned data. returndatacopy(0, 0, returndatasize()) switch result // delegatecall returns 0 on error. case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } /** * @dev This is a virtual function that should be overridden so it returns the address to which the fallback * function and {_fallback} should delegate. */ function _implementation() internal view virtual returns (address); /** * @dev Delegates the current call to the address returned by `_implementation()`. * * This function does not return to its internal call site, it will return directly to the external caller. */ function _fallback() internal virtual { _delegate(_implementation()); } /** * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other * function in the contract matches the call data. */ fallback() external payable virtual { _fallback(); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/transparent/ProxyAdmin.sol) pragma solidity ^0.8.20; import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol"; import {Ownable} from "../../access/Ownable.sol"; /** * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. */ contract ProxyAdmin is Ownable { /** * @dev The version of the upgrade interface of the contract. If this getter is missing, both `upgrade(address)` * and `upgradeAndCall(address,bytes)` are present, and `upgradeTo` must be used if no function should be called, * while `upgradeAndCall` will invoke the `receive` function if the second argument is the empty byte string. * If the getter returns `"5.0.0"`, only `upgradeAndCall(address,bytes)` is present, and the second argument must * be the empty byte string if no function should be called, making it impossible to invoke the `receive` function * during an upgrade. */ string public constant UPGRADE_INTERFACE_VERSION = "5.0.0"; /** * @dev Sets the initial owner who can perform upgrades. */ constructor(address initialOwner) Ownable(initialOwner) {} /** * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. * See {TransparentUpgradeableProxy-_dispatchUpgradeToAndCall}. * * Requirements: * * - This contract must be the admin of `proxy`. * - If `data` is empty, `msg.value` must be zero. */ function upgradeAndCall( ITransparentUpgradeableProxy proxy, address implementation, bytes memory data ) public payable virtual onlyOwner { proxy.upgradeToAndCall{value: msg.value}(implementation, data); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/transparent/TransparentUpgradeableProxy.sol) pragma solidity ^0.8.20; import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol"; import {ERC1967Proxy} from "../ERC1967/ERC1967Proxy.sol"; import {IERC1967} from "../../interfaces/IERC1967.sol"; import {ProxyAdmin} from "./ProxyAdmin.sol"; /** * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} * does not implement this interface directly, and its upgradeability mechanism is implemented by an internal dispatch * mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not * include them in the ABI so this interface must be used to interact with it. */ interface ITransparentUpgradeableProxy is IERC1967 { function upgradeToAndCall(address, bytes calldata) external payable; } /** * @dev This contract implements a proxy that is upgradeable through an associated {ProxyAdmin} instance. * * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector * clashing], which can potentially be used in an attack, this contract uses the * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two * things that go hand in hand: * * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if * that call matches the {ITransparentUpgradeableProxy-upgradeToAndCall} function exposed by the proxy itself. * 2. If the admin calls the proxy, it can call the `upgradeToAndCall` function but any other call won't be forwarded to * the implementation. If the admin tries to call a function on the implementation it will fail with an error indicating * the proxy admin cannot fallback to the target implementation. * * These properties mean that the admin account can only be used for upgrading the proxy, so it's best if it's a * dedicated account that is not used for anything else. This will avoid headaches due to sudden errors when trying to * call a function from the proxy implementation. For this reason, the proxy deploys an instance of {ProxyAdmin} and * allows upgrades only if they come through it. You should think of the `ProxyAdmin` instance as the administrative * interface of the proxy, including the ability to change who can trigger upgrades by transferring ownership. * * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not * inherit from that interface, and instead `upgradeToAndCall` is implicitly implemented using a custom dispatch * mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the * implementation. * * NOTE: This proxy does not inherit from {Context} deliberately. The {ProxyAdmin} of this contract won't send a * meta-transaction in any way, and any other meta-transaction setup should be made in the implementation contract. * * IMPORTANT: This contract avoids unnecessary storage reads by setting the admin only during construction as an * immutable variable, preventing any changes thereafter. However, the admin slot defined in ERC-1967 can still be * overwritten by the implementation logic pointed to by this proxy. In such cases, the contract may end up in an * undesirable state where the admin slot is different from the actual admin. * * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the * compiler will not check that there are no selector conflicts, due to the note above. A selector clash between any new * function and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This * could render the `upgradeToAndCall` function inaccessible, preventing upgradeability and compromising transparency. */ contract TransparentUpgradeableProxy is ERC1967Proxy { // An immutable address for the admin to avoid unnecessary SLOADs before each call // at the expense of removing the ability to change the admin once it's set. // This is acceptable if the admin is always a ProxyAdmin instance or similar contract // with its own ability to transfer the permissions to another account. address private immutable _admin; /** * @dev The proxy caller is the current admin, and can't fallback to the proxy target. */ error ProxyDeniedAdminAccess(); /** * @dev Initializes an upgradeable proxy managed by an instance of a {ProxyAdmin} with an `initialOwner`, * backed by the implementation at `_logic`, and optionally initialized with `_data` as explained in * {ERC1967Proxy-constructor}. */ constructor(address _logic, address initialOwner, bytes memory _data) payable ERC1967Proxy(_logic, _data) { _admin = address(new ProxyAdmin(initialOwner)); // Set the storage value and emit an event for ERC-1967 compatibility ERC1967Utils.changeAdmin(_proxyAdmin()); } /** * @dev Returns the admin of this proxy. */ function _proxyAdmin() internal virtual returns (address) { return _admin; } /** * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior. */ function _fallback() internal virtual override { if (msg.sender == _proxyAdmin()) { if (msg.sig != ITransparentUpgradeableProxy.upgradeToAndCall.selector) { revert ProxyDeniedAdminAccess(); } else { _dispatchUpgradeToAndCall(); } } else { super._fallback(); } } /** * @dev Upgrade the implementation of the proxy. See {ERC1967Utils-upgradeToAndCall}. * * Requirements: * * - If `data` is empty, `msg.value` must be zero. */ function _dispatchUpgradeToAndCall() private { (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); ERC1967Utils.upgradeToAndCall(newImplementation, data); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) pragma solidity ^0.8.20; /** * @dev Collection of functions related to the address type */ library Address { /** * @dev The ETH balance of the account is not enough to perform the operation. */ error AddressInsufficientBalance(address account); /** * @dev There's no code at `target` (it is not a contract). */ error AddressEmptyCode(address target); /** * @dev A call to an address target failed. The target may have reverted. */ error FailedInnerCall(); /** * @dev Replacement for Solidity's `transfer`: sends `amount` wei to * `recipient`, forwarding all available gas and reverting on errors. * * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost * of certain opcodes, possibly making contracts go over the 2300 gas limit * imposed by `transfer`, making them unable to receive funds via * `transfer`. {sendValue} removes this limitation. * * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. * * IMPORTANT: because control is transferred to `recipient`, care must be * taken to not create reentrancy vulnerabilities. Consider using * {ReentrancyGuard} or the * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. */ function sendValue(address payable recipient, uint256 amount) internal { if (address(this).balance < amount) { revert AddressInsufficientBalance(address(this)); } (bool success, ) = recipient.call{value: amount}(""); if (!success) { revert FailedInnerCall(); } } /** * @dev Performs a Solidity function call using a low level `call`. A * plain `call` is an unsafe replacement for a function call: use this * function instead. * * If `target` reverts with a revert reason or custom error, it is bubbled * up by this function (like regular Solidity function calls). However, if * the call reverted with no returned reason, this function reverts with a * {FailedInnerCall} error. * * Returns the raw returned data. To convert to the expected return value, * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. * * Requirements: * * - `target` must be a contract. * - calling `target` with `data` must not revert. */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { return functionCallWithValue(target, data, 0); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but also transferring `value` wei to `target`. * * Requirements: * * - the calling contract must have an ETH balance of at least `value`. * - the called Solidity function must be `payable`. */ function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { if (address(this).balance < value) { revert AddressInsufficientBalance(address(this)); } (bool success, bytes memory returndata) = target.call{value: value}(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but performing a static call. */ function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { (bool success, bytes memory returndata) = target.staticcall(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but performing a delegate call. */ function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { (bool success, bytes memory returndata) = target.delegatecall(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an * unsuccessful call. */ function verifyCallResultFromTarget( address target, bool success, bytes memory returndata ) internal view returns (bytes memory) { if (!success) { _revert(returndata); } else { // only check if target is a contract if the call was successful and the return data is empty // otherwise we already know that it was a contract if (returndata.length == 0 && target.code.length == 0) { revert AddressEmptyCode(target); } return returndata; } } /** * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the * revert reason or with a default {FailedInnerCall} error. */ function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { if (!success) { _revert(returndata); } else { return returndata; } } /** * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. */ function _revert(bytes memory returndata) private pure { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly /// @solidity memory-safe-assembly assembly { let returndata_size := mload(returndata) revert(add(32, returndata), returndata_size) } } else { revert FailedInnerCall(); } } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) pragma solidity ^0.8.20; /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract Context { function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } function _contextSuffixLength() internal view virtual returns (uint256) { return 0; } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol) // This file was procedurally generated from scripts/generate/templates/StorageSlot.js. pragma solidity ^0.8.20; /** * @dev Library for reading and writing primitive types to specific storage slots. * * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. * This library helps with reading and writing to such slots without the need for inline assembly. * * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. * * Example usage to set ERC1967 implementation slot: * ```solidity * contract ERC1967 { * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; * * function _getImplementation() internal view returns (address) { * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; * } * * function _setImplementation(address newImplementation) internal { * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } * ``` */ library StorageSlot { struct AddressSlot { address value; } struct BooleanSlot { bool value; } struct Bytes32Slot { bytes32 value; } struct Uint256Slot { uint256 value; } struct StringSlot { string value; } struct BytesSlot { bytes value; } /** * @dev Returns an `AddressSlot` with member `value` located at `slot`. */ function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `BooleanSlot` with member `value` located at `slot`. */ function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `Bytes32Slot` with member `value` located at `slot`. */ function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `Uint256Slot` with member `value` located at `slot`. */ function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `StringSlot` with member `value` located at `slot`. */ function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `StringSlot` representation of the string storage pointer `store`. */ function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := store.slot } } /** * @dev Returns an `BytesSlot` with member `value` located at `slot`. */ function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := slot } } /** * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. */ function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { /// @solidity memory-safe-assembly assembly { r.slot := store.slot } } }
File 2 of 2: Portal
// SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable2Step.sol) pragma solidity ^0.8.20; import {OwnableUpgradeable} from "./OwnableUpgradeable.sol"; import {Initializable} from "../proxy/utils/Initializable.sol"; /** * @dev Contract module which provides access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * The initial owner is specified at deployment time in the constructor for `Ownable`. This * can later be changed with {transferOwnership} and {acceptOwnership}. * * This module is used through inheritance. It will make available all functions * from parent (Ownable). */ abstract contract Ownable2StepUpgradeable is Initializable, OwnableUpgradeable { /// @custom:storage-location erc7201:openzeppelin.storage.Ownable2Step struct Ownable2StepStorage { address _pendingOwner; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable2Step")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant Ownable2StepStorageLocation = 0x237e158222e3e6968b72b9db0d8043aacf074ad9f650f0d1606b4d82ee432c00; function _getOwnable2StepStorage() private pure returns (Ownable2StepStorage storage $) { assembly { $.slot := Ownable2StepStorageLocation } } event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); function __Ownable2Step_init() internal onlyInitializing { } function __Ownable2Step_init_unchained() internal onlyInitializing { } /** * @dev Returns the address of the pending owner. */ function pendingOwner() public view virtual returns (address) { Ownable2StepStorage storage $ = _getOwnable2StepStorage(); return $._pendingOwner; } /** * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual override onlyOwner { Ownable2StepStorage storage $ = _getOwnable2StepStorage(); $._pendingOwner = newOwner; emit OwnershipTransferStarted(owner(), newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual override { Ownable2StepStorage storage $ = _getOwnable2StepStorage(); delete $._pendingOwner; super._transferOwnership(newOwner); } /** * @dev The new owner accepts the ownership transfer. */ function acceptOwnership() public virtual { address sender = _msgSender(); if (pendingOwner() != sender) { revert OwnableUnauthorizedAccount(sender); } _transferOwnership(sender); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol) pragma solidity ^0.8.20; import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol"; import {Initializable} from "../proxy/utils/Initializable.sol"; /** * @dev Contract module which provides a basic access control mechanism, where * there is an account (an owner) that can be granted exclusive access to * specific functions. * * The initial owner is set to the address provided by the deployer. This can * later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to * the owner. */ abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable { /// @custom:storage-location erc7201:openzeppelin.storage.Ownable struct OwnableStorage { address _owner; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300; function _getOwnableStorage() private pure returns (OwnableStorage storage $) { assembly { $.slot := OwnableStorageLocation } } /** * @dev The caller account is not authorized to perform an operation. */ error OwnableUnauthorizedAccount(address account); /** * @dev The owner is not a valid owner account. (eg. `address(0)`) */ error OwnableInvalidOwner(address owner); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ function __Ownable_init(address initialOwner) internal onlyInitializing { __Ownable_init_unchained(initialOwner); } function __Ownable_init_unchained(address initialOwner) internal onlyInitializing { if (initialOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(initialOwner); } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { _checkOwner(); _; } /** * @dev Returns the address of the current owner. */ function owner() public view virtual returns (address) { OwnableStorage storage $ = _getOwnableStorage(); return $._owner; } /** * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { if (owner() != _msgSender()) { revert OwnableUnauthorizedAccount(_msgSender()); } } /** * @dev Leaves the contract without owner. It will not be possible to call * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { if (newOwner == address(0)) { revert OwnableInvalidOwner(address(0)); } _transferOwnership(newOwner); } /** * @dev Transfers ownership of the contract to a new account (`newOwner`). * Internal function without access restriction. */ function _transferOwnership(address newOwner) internal virtual { OwnableStorage storage $ = _getOwnableStorage(); address oldOwner = $._owner; $._owner = newOwner; emit OwnershipTransferred(oldOwner, newOwner); } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/Initializable.sol) pragma solidity ^0.8.20; /** * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed * behind a proxy. Since proxied contracts do not make use of a constructor, it's common to move constructor logic to an * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. * * The initialization functions use a version number. Once a version number is used, it is consumed and cannot be * reused. This mechanism prevents re-execution of each "step" but allows the creation of new initialization steps in * case an upgrade adds a module that needs to be initialized. * * For example: * * [.hljs-theme-light.nopadding] * ```solidity * contract MyToken is ERC20Upgradeable { * function initialize() initializer public { * __ERC20_init("MyToken", "MTK"); * } * } * * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { * function initializeV2() reinitializer(2) public { * __ERC20Permit_init("MyToken"); * } * } * ``` * * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as * possible by providing the encoded function call as the `_data` argument to {ERC1967Proxy-constructor}. * * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. * * [CAUTION] * ==== * Avoid leaving a contract uninitialized. * * An uninitialized contract can be taken over by an attacker. This applies to both a proxy and its implementation * contract, which may impact the proxy. To prevent the implementation contract from being used, you should invoke * the {_disableInitializers} function in the constructor to automatically lock it when it is deployed: * * [.hljs-theme-light.nopadding] * ``` * /// @custom:oz-upgrades-unsafe-allow constructor * constructor() { * _disableInitializers(); * } * ``` * ==== */ abstract contract Initializable { /** * @dev Storage of the initializable contract. * * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions * when using with upgradeable contracts. * * @custom:storage-location erc7201:openzeppelin.storage.Initializable */ struct InitializableStorage { /** * @dev Indicates that the contract has been initialized. */ uint64 _initialized; /** * @dev Indicates that the contract is in the process of being initialized. */ bool _initializing; } // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant INITIALIZABLE_STORAGE = 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; /** * @dev The contract is already initialized. */ error InvalidInitialization(); /** * @dev The contract is not initializing. */ error NotInitializing(); /** * @dev Triggered when the contract has been initialized or reinitialized. */ event Initialized(uint64 version); /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. * * Similar to `reinitializer(1)`, except that in the context of a constructor an `initializer` may be invoked any * number of times. This behavior in the constructor can be useful during testing and is not expected to be used in * production. * * Emits an {Initialized} event. */ modifier initializer() { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); // Cache values to avoid duplicated sloads bool isTopLevelCall = !$._initializing; uint64 initialized = $._initialized; // Allowed calls: // - initialSetup: the contract is not in the initializing state and no previous version was // initialized // - construction: the contract is initialized at version 1 (no reininitialization) and the // current contract is just being deployed bool initialSetup = initialized == 0 && isTopLevelCall; bool construction = initialized == 1 && address(this).code.length == 0; if (!initialSetup && !construction) { revert InvalidInitialization(); } $._initialized = 1; if (isTopLevelCall) { $._initializing = true; } _; if (isTopLevelCall) { $._initializing = false; emit Initialized(1); } } /** * @dev A modifier that defines a protected reinitializer function that can be invoked at most once, and only if the * contract hasn't been initialized to a greater version before. In its scope, `onlyInitializing` functions can be * used to initialize parent contracts. * * A reinitializer may be used after the original initialization step. This is essential to configure modules that * are added through upgrades and that require initialization. * * When `version` is 1, this modifier is similar to `initializer`, except that functions marked with `reinitializer` * cannot be nested. If one is invoked in the context of another, execution will revert. * * Note that versions can jump in increments greater than 1; this implies that if multiple reinitializers coexist in * a contract, executing them in the right order is up to the developer or operator. * * WARNING: Setting the version to 2**64 - 1 will prevent any future reinitialization. * * Emits an {Initialized} event. */ modifier reinitializer(uint64 version) { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing || $._initialized >= version) { revert InvalidInitialization(); } $._initialized = version; $._initializing = true; _; $._initializing = false; emit Initialized(version); } /** * @dev Modifier to protect an initialization function so that it can only be invoked by functions with the * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { _checkInitializing(); _; } /** * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. */ function _checkInitializing() internal view virtual { if (!_isInitializing()) { revert NotInitializing(); } } /** * @dev Locks the contract, preventing any future reinitialization. This cannot be part of an initializer call. * Calling this in the constructor of a contract will prevent that contract from being initialized or reinitialized * to any version. It is recommended to use this to lock implementation contracts that are designed to be called * through proxies. * * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); if ($._initializing) { revert InvalidInitialization(); } if ($._initialized != type(uint64).max) { $._initialized = type(uint64).max; emit Initialized(type(uint64).max); } } /** * @dev Returns the highest version that has been initialized. See {reinitializer}. */ function _getInitializedVersion() internal view returns (uint64) { return _getInitializableStorage()._initialized; } /** * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. */ function _isInitializing() internal view returns (bool) { return _getInitializableStorage()._initializing; } /** * @dev Returns a pointer to the storage namespace. */ // solhint-disable-next-line var-name-mixedcase function _getInitializableStorage() private pure returns (InitializableStorage storage $) { assembly { $.slot := INITIALIZABLE_STORAGE } } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) pragma solidity ^0.8.20; import {Initializable} from "../proxy/utils/Initializable.sol"; /** * @dev Provides information about the current execution context, including the * sender of the transaction and its data. While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct * manner, since when dealing with meta-transactions the account sending and * paying for execution may not be the actual sender (as far as an application * is concerned). * * This contract is only required for intermediate, library-like contracts. */ abstract contract ContextUpgradeable is Initializable { function __Context_init() internal onlyInitializing { } function __Context_init_unchained() internal onlyInitializing { } function _msgSender() internal view virtual returns (address) { return msg.sender; } function _msgData() internal view virtual returns (bytes calldata) { return msg.data; } function _contextSuffixLength() internal view virtual returns (uint256) { return 0; } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol) pragma solidity ^0.8.20; /** * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. * * Adds the {permit} method, which can be used to change an account's ERC20 allowance (see {IERC20-allowance}) by * presenting a message signed by the account. By not relying on {IERC20-approve}, the token holder account doesn't * need to send a transaction, and thus is not required to hold Ether at all. * * ==== Security Considerations * * There are two important considerations concerning the use of `permit`. The first is that a valid permit signature * expresses an allowance, and it should not be assumed to convey additional meaning. In particular, it should not be * considered as an intention to spend the allowance in any specific way. The second is that because permits have * built-in replay protection and can be submitted by anyone, they can be frontrun. A protocol that uses permits should * take this into consideration and allow a `permit` call to fail. Combining these two aspects, a pattern that may be * generally recommended is: * * ```solidity * function doThingWithPermit(..., uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public { * try token.permit(msg.sender, address(this), value, deadline, v, r, s) {} catch {} * doThing(..., value); * } * * function doThing(..., uint256 value) public { * token.safeTransferFrom(msg.sender, address(this), value); * ... * } * ``` * * Observe that: 1) `msg.sender` is used as the owner, leaving no ambiguity as to the signer intent, and 2) the use of * `try/catch` allows the permit to fail and makes the code tolerant to frontrunning. (See also * {SafeERC20-safeTransferFrom}). * * Additionally, note that smart contract wallets (such as Argent or Safe) are not able to produce permit signatures, so * contracts should have entry points that don't rely on permit. */ interface IERC20Permit { /** * @dev Sets `value` as the allowance of `spender` over ``owner``'s tokens, * given ``owner``'s signed approval. * * IMPORTANT: The same issues {IERC20-approve} has related to transaction * ordering also apply here. * * Emits an {Approval} event. * * Requirements: * * - `spender` cannot be the zero address. * - `deadline` must be a timestamp in the future. * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` * over the EIP712-formatted function arguments. * - the signature must use ``owner``'s current nonce (see {nonces}). * * For more information on the signature format, see the * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP * section]. * * CAUTION: See Security Considerations above. */ function permit( address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; /** * @dev Returns the current nonce for `owner`. This value must be * included whenever a signature is generated for {permit}. * * Every successful call to {permit} increases ``owner``'s nonce by one. This * prevents a signature from being used multiple times. */ function nonces(address owner) external view returns (uint256); /** * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. */ // solhint-disable-next-line func-name-mixedcase function DOMAIN_SEPARATOR() external view returns (bytes32); } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) pragma solidity ^0.8.20; /** * @dev Interface of the ERC20 standard as defined in the EIP. */ interface IERC20 { /** * @dev Emitted when `value` tokens are moved from one account (`from`) to * another (`to`). * * Note that `value` may be zero. */ event Transfer(address indexed from, address indexed to, uint256 value); /** * @dev Emitted when the allowance of a `spender` for an `owner` is set by * a call to {approve}. `value` is the new allowance. */ event Approval(address indexed owner, address indexed spender, uint256 value); /** * @dev Returns the value of tokens in existence. */ function totalSupply() external view returns (uint256); /** * @dev Returns the value of tokens owned by `account`. */ function balanceOf(address account) external view returns (uint256); /** * @dev Moves a `value` amount of tokens from the caller's account to `to`. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transfer(address to, uint256 value) external returns (bool); /** * @dev Returns the remaining number of tokens that `spender` will be * allowed to spend on behalf of `owner` through {transferFrom}. This is * zero by default. * * This value changes when {approve} or {transferFrom} are called. */ function allowance(address owner, address spender) external view returns (uint256); /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the * caller's tokens. * * Returns a boolean value indicating whether the operation succeeded. * * IMPORTANT: Beware that changing an allowance with this method brings the risk * that someone may use both the old and the new allowance by unfortunate * transaction ordering. One possible solution to mitigate this race * condition is to first reduce the spender's allowance to 0 and set the * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * * Emits an {Approval} event. */ function approve(address spender, uint256 value) external returns (bool); /** * @dev Moves a `value` amount of tokens from `from` to `to` using the * allowance mechanism. `value` is then deducted from the caller's * allowance. * * Returns a boolean value indicating whether the operation succeeded. * * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 value) external returns (bool); } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; import {IERC20Permit} from "../extensions/IERC20Permit.sol"; import {Address} from "../../../utils/Address.sol"; /** * @title SafeERC20 * @dev Wrappers around ERC20 operations that throw on failure (when the token * contract returns false). Tokens that return no value (and instead revert or * throw on failure) are also supported, non-reverting calls are assumed to be * successful. * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. */ library SafeERC20 { using Address for address; /** * @dev An operation with an ERC20 token failed. */ error SafeERC20FailedOperation(address token); /** * @dev Indicates a failed `decreaseAllowance` request. */ error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); /** * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, * non-reverting calls are assumed to be successful. */ function safeTransfer(IERC20 token, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); } /** * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. */ function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); } /** * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. */ function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 oldAllowance = token.allowance(address(this), spender); forceApprove(token, spender, oldAllowance + value); } /** * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no * value, non-reverting calls are assumed to be successful. */ function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { unchecked { uint256 currentAllowance = token.allowance(address(this), spender); if (currentAllowance < requestedDecrease) { revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); } forceApprove(token, spender, currentAllowance - requestedDecrease); } } /** * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval * to be set to zero before setting it to a non-zero value, such as USDT. */ function forceApprove(IERC20 token, address spender, uint256 value) internal { bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); if (!_callOptionalReturnBool(token, approvalCall)) { _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); _callOptionalReturn(token, approvalCall); } } /** * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement * on the return value: the return value is optional (but if data is returned, it must not be false). * @param token The token targeted by the call. * @param data The call data (encoded using abi.encode or one of its variants). */ function _callOptionalReturn(IERC20 token, bytes memory data) private { // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that // the target address contains contract code and also asserts for success in the low-level call. bytes memory returndata = address(token).functionCall(data); if (returndata.length != 0 && !abi.decode(returndata, (bool))) { revert SafeERC20FailedOperation(address(token)); } } /** * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement * on the return value: the return value is optional (but if data is returned, it must not be false). * @param token The token targeted by the call. * @param data The call data (encoded using abi.encode or one of its variants). * * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. */ function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false // and not revert is the subcall reverts. (bool success, bytes memory returndata) = address(token).call(data); return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; } } // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) pragma solidity ^0.8.20; /** * @dev Collection of functions related to the address type */ library Address { /** * @dev The ETH balance of the account is not enough to perform the operation. */ error AddressInsufficientBalance(address account); /** * @dev There's no code at `target` (it is not a contract). */ error AddressEmptyCode(address target); /** * @dev A call to an address target failed. The target may have reverted. */ error FailedInnerCall(); /** * @dev Replacement for Solidity's `transfer`: sends `amount` wei to * `recipient`, forwarding all available gas and reverting on errors. * * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost * of certain opcodes, possibly making contracts go over the 2300 gas limit * imposed by `transfer`, making them unable to receive funds via * `transfer`. {sendValue} removes this limitation. * * https://consensys.net/diligence/blog/2019/09/stop-using-soliditys-transfer-now/[Learn more]. * * IMPORTANT: because control is transferred to `recipient`, care must be * taken to not create reentrancy vulnerabilities. Consider using * {ReentrancyGuard} or the * https://solidity.readthedocs.io/en/v0.8.20/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. */ function sendValue(address payable recipient, uint256 amount) internal { if (address(this).balance < amount) { revert AddressInsufficientBalance(address(this)); } (bool success, ) = recipient.call{value: amount}(""); if (!success) { revert FailedInnerCall(); } } /** * @dev Performs a Solidity function call using a low level `call`. A * plain `call` is an unsafe replacement for a function call: use this * function instead. * * If `target` reverts with a revert reason or custom error, it is bubbled * up by this function (like regular Solidity function calls). However, if * the call reverted with no returned reason, this function reverts with a * {FailedInnerCall} error. * * Returns the raw returned data. To convert to the expected return value, * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`]. * * Requirements: * * - `target` must be a contract. * - calling `target` with `data` must not revert. */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { return functionCallWithValue(target, data, 0); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but also transferring `value` wei to `target`. * * Requirements: * * - the calling contract must have an ETH balance of at least `value`. * - the called Solidity function must be `payable`. */ function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { if (address(this).balance < value) { revert AddressInsufficientBalance(address(this)); } (bool success, bytes memory returndata) = target.call{value: value}(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but performing a static call. */ function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { (bool success, bytes memory returndata) = target.staticcall(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], * but performing a delegate call. */ function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { (bool success, bytes memory returndata) = target.delegatecall(data); return verifyCallResultFromTarget(target, success, returndata); } /** * @dev Tool to verify that a low level call to smart-contract was successful, and reverts if the target * was not a contract or bubbling up the revert reason (falling back to {FailedInnerCall}) in case of an * unsuccessful call. */ function verifyCallResultFromTarget( address target, bool success, bytes memory returndata ) internal view returns (bytes memory) { if (!success) { _revert(returndata); } else { // only check if target is a contract if the call was successful and the return data is empty // otherwise we already know that it was a contract if (returndata.length == 0 && target.code.length == 0) { revert AddressEmptyCode(target); } return returndata; } } /** * @dev Tool to verify that a low level call was successful, and reverts if it wasn't, either by bubbling the * revert reason or with a default {FailedInnerCall} error. */ function verifyCallResult(bool success, bytes memory returndata) internal pure returns (bytes memory) { if (!success) { _revert(returndata); } else { return returndata; } } /** * @dev Reverts with returndata if present. Otherwise reverts with {FailedInnerCall}. */ function _revert(bytes memory returndata) private pure { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly /// @solidity memory-safe-assembly assembly { let returndata_size := mload(returndata) revert(add(32, returndata), returndata_size) } } else { revert FailedInnerCall(); } } } // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.24; /// @notice An interface to be optionally implemented by ERC20 tokens to retrieve /// the decimals places of the token. We need this interface because to /// properly calculate amounts when 2 tokens are involved we need to know /// their decimals places. /// As decimals() is optional in the ERC20 standard, additional interface /// is needed to retrieve the decimals places of the token. /// Portal contract assumes that only tokens supporting this interface /// are accepted as supported tokens or receipt tokens. interface IERC20WithDecimals { /// @notice Returns the decimals places of the token function decimals() external view returns (uint8); } // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.24; /// @notice An interface to be optionally implemented by ERC20 tokens to enable /// gasless approvals. interface IERC20WithPermit { /// @notice EIP2612 approval made with secp256k1 signature. /// Users can authorize a transfer of their tokens with a signature /// conforming EIP712 standard, rather than an on-chain transaction /// from their address. Anyone can submit this signature on the /// user's behalf by calling the permit function, paying gas fees, /// and possibly performing other actions in the same transaction. function permit( address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s ) external; } // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.24; /// @notice IReceiptToken is an interface that should be implemented by ERC20 /// receipt tokens used in the Portal contract. interface IReceiptToken { /// @notice Mints `amount` receipt tokens and transfers them to the `to` /// address. function mintReceipt(address to, uint256 amount) external; /// @notice Burns `amount` of receipt tokens owned by `msg.sender`. function burnReceipt(uint256 amount) external; } // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.24; /// @notice An interface that should be implemented by contracts supporting /// `approveAndCall`/`receiveApproval` pattern. interface IReceiveApproval { /// @notice Receives approval to spend tokens. Called as a result of /// `approveAndCall` call on the token. function receiveApproval( address from, uint256 amount, address token, bytes calldata extraData ) external; } // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.24; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "./interfaces/IReceiveApproval.sol"; import "./interfaces/IReceiptToken.sol"; import "./interfaces/IERC20WithPermit.sol"; import "./interfaces/IERC20WithDecimals.sol"; contract Portal is Ownable2StepUpgradeable, IReceiveApproval { using SafeERC20 for IERC20; /// @notice Supported token ability defines what can be done with /// a given token. Some tokens can be deposited but can not be locked. enum TokenAbility { None, Deposit, DepositAndLock } /// @notice Represents the to-tBTC migration state of a deposit. Note that /// to-tBTC migration is only available for selected assets, as set /// by the governance. enum TbtcMigrationState { NotRequested, Requested, InProgress, Completed } /// @notice Supported token struct to pair a token address with its ability /// @dev This is an auxiliary struct used to initialize the contract with /// the supported tokens and for governance to add new supported tokens. /// This struct is not used in the storage. struct SupportedToken { address token; TokenAbility tokenAbility; } /// @notice Groups depositor address to their deposit ID to pass in an array /// to trigger and complete the to-tBTC migration by the tBTC /// migration treasury. /// @dev This is an auxiliary struct used as a parameter in the /// `withdrawForTbtcMigration` and `finalizeTbtcMigration` functions. /// This struct is not used in the storage. struct DepositToMigrate { address depositor; uint256 depositId; } /// @notice DepositInfo keeps track of the deposit balance and unlock time. /// Each deposit is tracked separately and associated with a specific /// token. Some tokens can be deposited but can not be locked - in /// that case the unlockAt is the block timestamp of when the deposit /// was created. The same is true for tokens that can be locked but /// the depositor decided not to lock them. Some deposits can mint /// a receipt tokens against them: receiptMinted is the amount of /// receipt tokens minted against a deposit, while feeOwed is the /// fee owed by the deposit to Portal, and the lastFeeIntegral is /// the last updated value of the fee integral. struct DepositInfo { uint96 balance; uint32 unlockAt; // The amount of the receipt token minted for the deposit. Zero if no // receipt token was minted. uint96 receiptMinted; // The fee owed if the receipt token was minted and the fee is non-zero. // Fee owed is calculated based on the receipt tokens minted and is stored // using receipt token decimals. The fee is collected when the deposit is // withdrawn, adjusted to the deposit token decimals. uint96 feeOwed; // The last value of the receipt token fee integral. Zero if no receipt // token was minted, if the fee is zero, or if the integral was not yet // calculated. uint88 lastFeeIntegral; // The state of tBTC migration. If the to-tBTC migration is not allowed // for the asset or if it was not requested by the depositor, the state // is NotRequested. TbtcMigrationState tbtcMigrationState; } /// @notice Keeps information about to-tBTC migration status for the asset: /// whether the migration is allowed and what is the total amount /// of the asset being currently in-progress for the migration. struct TbtcMigrationInfo { // Indicates whether the migration to tBTC was enabled by the governance // for the asset. bool isAllowed; // The total amount that is currently migrated to tBTC. The value uses // the same number of decimals as the asset being migrated. uint96 totalMigrating; } /// @notice FeeInfo keeps track of the receipt token minting situation per /// the supported deposit token. /// /// The current fee rate for minting against deposits is /// calculated separately for each token, and tracked as integral. /// /// The fee rate has a single component: a governable base annual /// fee, capped at 100%. /// /// The annual fee is converted to a non-compounding fee per second, /// R. This rate is then multiplied by time in seconds to get the /// accumulated fee integral. The difference between the integral /// in time T_1 and T_2 tracks how much fee is accrued per minted /// token unit during that time. The minted amount A is multiplied /// by R, and divided by a scalar constant to get the amount of fee /// in the same units as A. struct FeeInfo { // XXX: not tracking total deposited - use token balance // someone could send token to the contract outside locked deposits // The amount of receipt tokens minted across all deposits for the given // deposit token. uint96 totalMinted; // The timestamp of the block of the last fee update. uint32 lastFeeUpdateAt; // The integral tracks how much fee is accrued over time. uint88 feeIntegral; // Fee per annum in percentage. uint8 annualFee; // Receipt token minting cap in percentage, per deposit. uint8 mintCap; // A token supporting the IReceiptToken interface and used as a receipt // token. Zero address if the given deposit token does not allow minting // receipt tokens. address receiptToken; // Counts fees collected across all withdrawn deposits for the given // deposit token. Fees are collected based on the receipt tokens earlier // minted and the annual fee. uint96 feeCollected; } /// @notice Mapping of the details of all the deposited funds: /// depositor address -> token address -> deposit id -> DepositInfo mapping(address => mapping(address => mapping(uint256 => DepositInfo))) public deposits; /// @notice The number of deposits created. Includes the deposits that /// were fully withdrawn. This is also the identifier of the most /// recently created deposit. uint256 public depositCount; /// @notice List of tokens enabled for deposit or for deposit and lock. mapping(address => TokenAbility) public tokenAbility; /// @notice Minimum time a deposit can be locked. uint32 public minLockPeriod; /// @notice Maximum time a deposit can be locked. uint32 public maxLockPeriod; /// @notice Address of the liquidity treasury multisig. address public liquidityTreasury; /// @notice Mapping to store whether an asset is managed by the liquidity /// treasury multisig. mapping(address => bool) public liquidityTreasuryManaged; /// @notice Mapping of depositable tokens to their receipt token fee /// parameters. mapping(address => FeeInfo) public feeInfo; /// @notice Address of the tBTC token. Used for the to-tBTC migration of /// deposited assets for which the migration was enabled by the /// governance. address public tbtcToken; /// @notice Address of the tBTC migration treasury multisig. The tBTC /// migration treasury is responsible for conducting the /// to-tBTC migration of deposited assets for which the migration /// was enabled by the governance. address public tbtcMigrationTreasury; /// @notice Mapping indicating whether the given asset can be migrated to /// tBTC by the tBTC migration treasury and if so, what is the /// global status of migrations. The key to the mapping is the /// address of the asset being migrated. mapping(address => TbtcMigrationInfo) public tbtcMigrations; event Deposited( address indexed depositor, address indexed token, uint256 indexed depositId, uint256 amount ); event Withdrawn( address indexed depositor, address indexed token, uint256 indexed depositId, uint256 amount ); event Locked( address indexed depositor, address indexed token, uint256 indexed depositId, uint32 unlockAt, uint32 lockPeriod ); event SupportedTokenAdded(address indexed token, TokenAbility tokenAbility); event MaxLockPeriodUpdated(uint32 maxLockPeriod); event MinLockPeriodUpdated(uint32 minLockPeriod); /// @notice Event emitted when the liquidity treasury address is updated. event LiquidityTreasuryUpdated( address indexed previousLiquidityTreasury, address indexed newLiquidityTreasury ); /// @notice Event emitted when an asset is withdrawn by the liquidity /// treasury multisig. event WithdrawnByLiquidityTreasury(address indexed token, uint256 amount); /// @notice Event emitted when an asset is set/unset as a liquidity /// treasury multisig managed asset. event LiquidityTreasuryManagedAssetUpdated( address indexed asset, bool isManaged ); /// @notice Event emitted when the receipt parameters for a deposit token /// are updated. event ReceiptParamsUpdated( address indexed token, uint8 annualFee, uint8 mintCap, address receiptToken ); /// @notice Event emitted when a receipt token is minted for a deposit. event ReceiptMinted( address indexed depositor, address indexed token, uint256 indexed depositId, uint256 amount ); /// @notice Event emitted when a receipt token is repaid for a deposit. event ReceiptRepaid( address indexed depositor, address indexed token, uint256 indexed depositId, uint256 amount ); /// @notice Event emitted when a fee is collected for a deposit. event FeeCollected( address indexed depositor, address indexed token, uint256 indexed depositId, uint256 fee ); /// @notice Event emitted when the tBTC token address used for the /// to-tBTC migration was set by the governance. /// @dev This event can be emitted only one time. event TbtcTokenAddressSet(address tbtc); /// @notice Event emitted when the to-tBTC migration treasury address was /// updated. event TbtcMigrationTreasuryUpdated( address indexed previousMigrationTreasury, address indexed newMigrationTreasury ); /// @notice Event emitted when the eligibility for the to-tBTC migration /// for the asset was updated. event TbtcMigrationAllowedUpdated(address indexed token, bool isAllowed); /// @notice Event emitted when the to-tBTC migration was requested for the /// deposit. event TbtcMigrationRequested( address indexed depositor, address indexed token, uint256 indexed depositId ); /// @notice Event emitted when the to-tBTC migration was started for the /// deposit. event TbtcMigrationStarted( address indexed depositor, address indexed token, uint256 indexed depositId ); /// @notice Event emitted when the tBTC migration treasury started to-tBTC /// migration for a set of deposits withdrawing the given amount of /// the token from the Portal contract. event WithdrawnForTbtcMigration(address indexed token, uint256 amount); /// @notice Event emitted when the to-tBTC migration was completed for the /// deposit. event TbtcMigrationCompleted( address indexed depositor, address indexed token, uint256 indexed depositId ); /// @notice Event emitted when the to-tBTC migration was completed for one /// or multiple deposits and the Portal contract was funded with /// tBTC coming from the migrated assets. /// @dev The `amount` in the event is the amount in tBTC, using tBTC token's /// precision. event FundedFromTbtcMigration(uint256 amount); /// @notice Event emitted in similar circumstances as `Withdrawn` except /// that it represents a to-tBTC migrated deposit where the /// withdrawal is performed in tBTC and not in the original deposit /// token. event WithdrawnTbtcMigrated( address indexed depositor, address indexed token, address tbtcToken, uint256 indexed depositId, uint256 amountInTbtc ); /// @notice Event emitted in similar circumstances as `FeeCollected` /// except that it represents a to-tBTC migrated deposit where the /// fee is collected in tBTC and not in the original deposit token. event FeeCollectedTbtcMigrated( address indexed depositor, address indexed token, address tbtcToken, uint256 indexed depositId, uint256 feeInTbtc ); error IncorrectTokenAddress(address token); error IncorrectTokenAbility(TokenAbility ability); error IncorrectDepositor(address depositor); error IncorrectAmount(uint256 amount); error TokenNotSupported(address token); error TokenAlreadySupported(address token, TokenAbility tokenAbility); error InsufficientTokenAbility(address token, TokenAbility tokenAbility); error LockPeriodOutOfRange(uint32 lockPeriod); error IncorrectLockPeriod(uint256 lockPeriod); error LockPeriodTooShort( uint32 lockPeriod, uint32 newUnlockAt, uint32 existingUnlockAt ); error DepositLocked(uint32 unlockAt); error DepositNotFound(); /// @notice Error when the partial withdrawal amount is too high. Either it /// equals the deposit amount (not a partial withdrawal) or it is /// higher than the deposit amount. error PartialWithdrawalAmountTooHigh(uint256 depositAmount); /// @notice Error when the sender is not the liquidity treasury. error SenderNotLiquidityTreasury(address sender); /// @notice Error when the asset is not managed by the liquidity treasury. error AssetNotManagedByLiquidityTreasury(address asset); /// @notice Error when trying to withdraw without repaying the minted /// receipt tokens back to Portal. error ReceiptNotRepaid(uint256 receiptMinted); /// @notice Error when the user tries to partially withdraw but the fee for /// minting the receipt token is non-zero. In this case only a full /// deposit withdrawal is possible. error ReceiptFeeOwed(uint256 feeOwed); /// @notice Error when the user tries to mint more than the mint limit /// allows. error ReceiptMintLimitExceeded( uint256 mintLimit, uint96 currentlyMinted, uint256 feeOwed, uint256 amount ); /// @notice Error when the user tries to repay more than the minted debt. error RepayAmountExceededDebt(uint96 mintedDebt, uint256 amount); /// @notice Error when the annual fee proposed exceeds 100%. error MaxAnnualFeeExceeded(uint8 annualFee); /// @notice Error when the mint cap proposed exceeds 100%. error MaxReceiptMintCapExceeded(uint8 mintCap); /// @notice Error when there is no receipt token set for the asset. error ReceiptMintingDisabled(); /// @notice Error when the receipt token is already initialized. error ReceiptTokenAlreadyInitialized(); /// @notice Error when the receipt token decimals are not 18. error IncorrectReceiptTokenDecimals(address receiptToken); /// @notice Error when token being accepted as deposit token does not /// support decimals() function or the function is malfunctioning. error UnknownTokenDecimals(address token); /// @notice The given asset can be marked as eligible for tBTC migration if /// it is not managed by the liquidity treasury. The given asset /// can be managed by the liquidity treasury if it is not marked as /// eligible for tBTC migration. The transaction reverts with this /// error when the governance changes would conflict with this /// limitation. error TbtcMigrationAndLiquidityManagementConflict(); /// @notice Error when the governance tries to add tBTC as an asset eligible /// for to-tBTC migration. error TbtcCanNotBeMigrated(); /// @notice Error when the tBTC token address used for to-tBTC migrations /// was already set by the governance. error TbtcTokenAddressAlreadySet(); /// @notice Error when the tBTC token address used for to-tBTC migrations /// was not set by the governance but there is an attempt to enable /// to-tBTC migration for some asset. error TbtcTokenAddressNotSet(); /// @notice Error when someone else but the tBTC migration treasury tried /// to withdraw tokens for tBTC migration or to complete the /// migration process. error SenderNotTbtcMigrationTreasury(); /// @notice Error when the given to-tBTC transition state change is /// requested from an unexpected state. For example, it is not /// allowed to complete the migration of a deposit for which the /// migration was not requested. error UnexpectedTbtcMigrationState( uint256 depositId, TbtcMigrationState currentState, TbtcMigrationState expectedState ); /// @notice Error when the to-tBTC migration for the asset was not allowed /// by the governance but the depositor tried to request a migration. error TbtcMigrationNotAllowed(); /// @notice Error when the to-tBTC migration has been requested but not yet /// completed and the depositor tries to withdraw the deposit. error TbtcMigrationNotCompleted(); /// @notice Error when the to-tBTC migration was requested and the depositor /// tries to partially withdraw the deposit. /// @dev The `Err` suffix is to distinguish this error from the /// `TbtcMigrationRequested` event. error TbtcMigrationRequestedErr(); /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); } /// @notice Initialize the contract with the supported tokens and their /// abilities. /// @dev We are using OpenZeppelin's initializer pattern to initialize /// the contract. This function is called only once during the contract /// deployment. /// @param supportedTokens List of supported tokens and their abilities function initialize( SupportedToken[] memory supportedTokens ) external initializer { __Ownable_init(msg.sender); minLockPeriod = 4 weeks; maxLockPeriod = 39 weeks; depositCount = 0; for (uint256 i = 0; i < supportedTokens.length; i++) { address token = supportedTokens[i].token; TokenAbility ability = supportedTokens[i].tokenAbility; if (token == address(0)) { revert IncorrectTokenAddress(token); } tokenAbility[token] = ability; } } /// @notice Add a new supported token and define its ability. /// @dev Only the owner can add a new supported token. Supported token must /// implement the decimals() method so Portal is able to perform /// correct calculations. /// @param supportedToken Supported token and its ability function addSupportedToken( SupportedToken calldata supportedToken ) external onlyOwner { address token = supportedToken.token; TokenAbility ability = supportedToken.tokenAbility; if (token == address(0)) { revert IncorrectTokenAddress(token); } if (ability == TokenAbility.None) { revert IncorrectTokenAbility(ability); } if (tokenAbility[token] != TokenAbility.None) { revert TokenAlreadySupported(token, tokenAbility[token]); } // Attempt to call decimals() on the token to verify it exists // slither-disable-next-line unused-return try IERC20WithDecimals(token).decimals() {} catch { revert UnknownTokenDecimals(token); } tokenAbility[token] = ability; emit SupportedTokenAdded(token, ability); } /// @notice Set the liquidity treasury multisig address. /// @dev Only the owner can set the liquidity treasury multisig address. It /// is possible to set the liquidity treasury multisig address to zero /// in case the liquidity mining should be disabled. /// @param _liquidityTreasury address of the liquidity treasury multisig function setLiquidityTreasury( address _liquidityTreasury ) external onlyOwner { emit LiquidityTreasuryUpdated(liquidityTreasury, _liquidityTreasury); // slither-disable-next-line missing-zero-check liquidityTreasury = _liquidityTreasury; } /// @notice Set whether an asset is managed by the liquidity treasury multisig. /// @dev Only the owner can set whether an asset is managed by the liquidity /// treasury multisig. Only assets which can be locked in the Portal can /// be managed by the liquidity treasury multisig. /// @param asset address of the asset to be managed by the liquidity treasury /// @param isManaged boolean value to set whether the asset is managed by the /// liquidity treasury multisig function setAssetAsLiquidityTreasuryManaged( address asset, bool isManaged ) external onlyOwner { TokenAbility ability = tokenAbility[asset]; if (ability == TokenAbility.None) { revert TokenNotSupported(asset); } if (ability != TokenAbility.DepositAndLock) { revert InsufficientTokenAbility(asset, tokenAbility[asset]); } if (tbtcMigrations[asset].isAllowed) { revert TbtcMigrationAndLiquidityManagementConflict(); } liquidityTreasuryManaged[asset] = isManaged; emit LiquidityTreasuryManagedAssetUpdated(asset, isManaged); } /// @notice Set the tBTC token address. Setting the tBTC token address /// allows the governance to enable to-tBTC migrations of selected /// deposited assets. /// @dev Can only be executed one time. Can only be executed by the /// governance. /// @param _tbtcToken address of the tBTC token function setTbtcTokenAddress(address _tbtcToken) external onlyOwner { if (_tbtcToken == address(0)) { revert IncorrectTokenAddress(_tbtcToken); } if (tbtcToken != address(0)) { revert TbtcTokenAddressAlreadySet(); } tbtcToken = _tbtcToken; emit TbtcTokenAddressSet(_tbtcToken); } /// @notice Set the tBTC migration treasury multisig address. /// @dev Only the owner can set the tBTC migraton treasury multisig address. /// It is possible to set the migration treasury address to zero in /// case the migration should be disabled. /// @param _tbtcMigrationTreasury the new address of the tBTC migration /// treasury multisig function setTbtcMigrationTreasury( address _tbtcMigrationTreasury ) external onlyOwner { emit TbtcMigrationTreasuryUpdated( tbtcMigrationTreasury, _tbtcMigrationTreasury ); // slither-disable-next-line missing-zero-check tbtcMigrationTreasury = _tbtcMigrationTreasury; } /// @notice Set whether an asset can be migrated to tBTC by the migration /// treasury multisig. Only assets that can be deposited in the /// Portal can be marked as eligible for migration. Only assets that /// represent Bitcoin and can be mapped 1:1 to tBTC can be marked /// as eligible for migration. /// @param asset address of the asset to upgrade the to-tBTC migration /// settings for /// @param isAllowed boolean value indicating whether to-tBTC migration /// is allowed function setAssetTbtcMigrationAllowed( address asset, bool isAllowed ) external onlyOwner { if (tbtcToken == address(0)) { revert TbtcTokenAddressNotSet(); } if (asset == tbtcToken) { revert TbtcCanNotBeMigrated(); } TokenAbility ability = tokenAbility[asset]; if (ability == TokenAbility.None) { revert TokenNotSupported(asset); } if (liquidityTreasuryManaged[asset]) { revert TbtcMigrationAndLiquidityManagementConflict(); } tbtcMigrations[asset].isAllowed = isAllowed; emit TbtcMigrationAllowedUpdated(asset, isAllowed); } /// @notice Set the minimum lock period for deposits. /// @dev Only the owner can update the minimum lock period. The new value /// must be normalized to weeks, non-zero, and not higher than the /// maximum lock period. /// @param _minLockPeriod new minimum lock period function setMinLockPeriod(uint32 _minLockPeriod) external onlyOwner { uint32 normalizedLockPeriod = _normalizeLockPeriod(_minLockPeriod); if ( _minLockPeriod != normalizedLockPeriod || normalizedLockPeriod == 0 || _minLockPeriod > maxLockPeriod ) { revert IncorrectLockPeriod(_minLockPeriod); } minLockPeriod = _minLockPeriod; emit MinLockPeriodUpdated(_minLockPeriod); } /// @notice Set the maximum lock period for deposits. Maximum lock /// period is used as a limit to prevent users from accidentally /// locking their deposits for too long. /// @dev Only the owner can update the maximum lock period. The new value /// must be normalized to weeks and not lower than the minimum lock /// period. /// @param _maxLockPeriod new maximum lock period function setMaxLockPeriod(uint32 _maxLockPeriod) external onlyOwner { if ( _maxLockPeriod != _normalizeLockPeriod(_maxLockPeriod) || _maxLockPeriod < minLockPeriod ) { revert IncorrectLockPeriod(_maxLockPeriod); } maxLockPeriod = _maxLockPeriod; emit MaxLockPeriodUpdated(_maxLockPeriod); } /// @notice Set the receipt parameters for a supported deposit token. The /// receipt token address can be set only one time. All following /// calls to this function for the same deposit token needs to pass /// the same address of the receipt token as set initially. /// @dev Only the owner can set the receipt parameters. Receipt token must /// implement the decimals() method so Portal is able to perform correct /// calculations for minting receipt tokens. /// @param token deposit token address to set the parameters /// @param annualFee annual fee in percentage for minting the receipt token /// @param mintCap mint cap in percentage for minting the receipt token /// @param receiptToken receipt token address function setReceiptParams( address token, uint8 annualFee, uint8 mintCap, address receiptToken ) external onlyOwner { if (tokenAbility[token] == TokenAbility.None) { revert TokenNotSupported(token); } if (annualFee > 100) { revert MaxAnnualFeeExceeded(annualFee); } if (mintCap > 100) { revert MaxReceiptMintCapExceeded(mintCap); } if (receiptToken == address(0)) { revert IncorrectTokenAddress(receiptToken); } FeeInfo storage i = feeInfo[token]; if (i.receiptToken != address(0) && i.receiptToken != receiptToken) { revert ReceiptTokenAlreadyInitialized(); } uint8 decimals = IERC20WithDecimals(receiptToken).decimals(); if (decimals != 18) { revert IncorrectReceiptTokenDecimals(receiptToken); } _updateFeeIntegral(token); i.annualFee = annualFee; i.mintCap = mintCap; i.receiptToken = receiptToken; // solhint-disable-next-line not-rely-on-time i.lastFeeUpdateAt = uint32(block.timestamp); emit ReceiptParamsUpdated(token, annualFee, mintCap, receiptToken); } /// @notice Withdraws all deposited tokens. /// /// Deposited lockable tokens can be withdrawn at any time if /// there is no lock set on the deposit or the lock period has passed. /// There is no way to withdraw locked deposit. Tokens that are not /// lockable can be withdrawn at any time. /// /// Deposits for which receipt tokens were minted and not fully /// repaid can not be withdrawn even if the lock expired. Repaying /// all receipt tokens is a must to withdraw the deposit. Upon /// withdrawing a deposit for which the receipt tokens were minted, /// the fee is collected based on the annual fee and the amount /// of minted receipt tokens. /// /// Deposits for which to-tBTC migration was requested but not yet /// completed yet can not be withdrawn. The deposits for which the /// to-tBTC migration was completed are withdrawn in tBTC. /// /// This function withdraws all deposited tokens. For partial /// withdrawals, use `withdrawPartially`. /// @param token deposited token address /// @param depositId id of the deposit function withdraw(address token, uint256 depositId) external { TokenAbility ability = tokenAbility[token]; if (ability == TokenAbility.None) { revert TokenNotSupported(token); } DepositInfo storage selectedDeposit = deposits[msg.sender][token][ depositId ]; if ( selectedDeposit.tbtcMigrationState == TbtcMigrationState.Requested || selectedDeposit.tbtcMigrationState == TbtcMigrationState.InProgress ) { revert TbtcMigrationNotCompleted(); } if ( ability == TokenAbility.DepositAndLock && // solhint-disable-next-line not-rely-on-time block.timestamp < selectedDeposit.unlockAt ) { revert DepositLocked(selectedDeposit.unlockAt); } if (selectedDeposit.receiptMinted > 0) { revert ReceiptNotRepaid(selectedDeposit.receiptMinted); } uint96 depositedAmount = selectedDeposit.balance; if (depositedAmount == 0) { revert DepositNotFound(); } if ( selectedDeposit.tbtcMigrationState == TbtcMigrationState.Completed ) { uint96 feeInTbtc = 0; // The fee is collected based on the pre-migration rules, so based // on the annual fee as set by the governance for the original token. // The fee is collected in tBTC as the entire deposit was migrated // to tBTC. if (selectedDeposit.feeOwed > 0) { FeeInfo storage tokenFeeInfo = feeInfo[token]; feeInTbtc = _adjustTokenDecimals( tokenFeeInfo.receiptToken, tbtcToken, selectedDeposit.feeOwed ); } uint96 depositedAmountInTbtc = _adjustTokenDecimals( token, tbtcToken, depositedAmount ); uint96 withdrawableInTbtc = depositedAmountInTbtc - feeInTbtc; emit WithdrawnTbtcMigrated( msg.sender, token, tbtcToken, depositId, withdrawableInTbtc ); emit FeeCollectedTbtcMigrated( msg.sender, token, tbtcToken, depositId, feeInTbtc ); feeInfo[tbtcToken].feeCollected += feeInTbtc; delete deposits[msg.sender][token][depositId]; IERC20(tbtcToken).safeTransfer(msg.sender, withdrawableInTbtc); } else { uint96 fee = 0; if (selectedDeposit.feeOwed > 0) { FeeInfo storage tokenFeeInfo = feeInfo[token]; fee = _adjustTokenDecimals( tokenFeeInfo.receiptToken, token, selectedDeposit.feeOwed ); } uint96 withdrawable = depositedAmount - fee; emit Withdrawn(msg.sender, token, depositId, withdrawable); emit FeeCollected(msg.sender, token, depositId, fee); feeInfo[token].feeCollected += fee; delete deposits[msg.sender][token][depositId]; IERC20(token).safeTransfer(msg.sender, withdrawable); } } /// @notice Withdraws part of the deposited tokens. /// /// Deposited lockable tokens can be withdrawn at any time if /// there is no lock set on the deposit or the lock period has passed. /// There is no way to withdraw locked deposit. Tokens that are not /// lockable can be withdrawn at any time. /// /// Deposits for which receipt tokens were minted and fully repaid /// can not be partially withdrawn even if the lock expired. /// Repaying all receipt tokens is a must to partially withdraw the /// deposit. If the fee for receipt tokens minted is non-zero, the /// deposit can not be partially withdrawn and only a full /// withdrawal is possible. /// /// Deposits for which the to-tBTC migration was requested can not /// be withdrawn partially. /// /// This function allows only for partial withdrawals. For full /// withdrawals, use `withdraw`. /// @param token deposited token address /// @param depositId id of the deposit /// @param amount the amount to be withdrawn function withdrawPartially( address token, uint256 depositId, uint96 amount ) external { TokenAbility ability = tokenAbility[token]; if (ability == TokenAbility.None) { revert TokenNotSupported(token); } if (amount == 0) { revert IncorrectAmount(amount); } DepositInfo storage selectedDeposit = deposits[msg.sender][token][ depositId ]; if (selectedDeposit.balance == 0) { revert DepositNotFound(); } if (amount >= selectedDeposit.balance) { revert PartialWithdrawalAmountTooHigh(selectedDeposit.balance); } if ( ability == TokenAbility.DepositAndLock && // solhint-disable-next-line not-rely-on-time block.timestamp < selectedDeposit.unlockAt ) { revert DepositLocked(selectedDeposit.unlockAt); } if (selectedDeposit.receiptMinted > 0) { revert ReceiptNotRepaid(selectedDeposit.receiptMinted); } if (selectedDeposit.feeOwed > 0) { revert ReceiptFeeOwed(selectedDeposit.feeOwed); } if ( selectedDeposit.tbtcMigrationState != TbtcMigrationState.NotRequested ) { revert TbtcMigrationRequestedErr(); } emit Withdrawn(msg.sender, token, depositId, amount); selectedDeposit.balance -= amount; IERC20(token).safeTransfer(msg.sender, amount); } /// @notice Receive approval from a token contract and deposit the tokens in /// one transaction. If the the token is lockable and lock period is /// greater than 0, the deposit will be locked for the given period. /// @dev This function is called by the token following the `approveAndCall` /// pattern. // Encoded lock period is counted in seconds, will be normalized to /// weeks. If non-zero, it must not be shorter than the minimum lock /// period and must not be longer than the maximum lock period. To not /// lock the deposit, the lock period must be 0. /// @param from address which approved and sent the tokens /// @param amount amount of tokens sent /// @param token address of the token contract /// @param data encoded lock period function receiveApproval( address from, uint256 amount, address token, bytes calldata data ) external override { if (token != msg.sender) { revert IncorrectTokenAddress(token); } if (amount > type(uint96).max) { revert IncorrectAmount(amount); } uint32 lockPeriod = abi.decode(data, (uint32)); _depositFor(from, from, token, uint96(amount), lockPeriod); } /// @notice Deposit and optionally lock tokens for the given period. /// @dev Lock period will be normalized to weeks. If non-zero, it must not /// be shorter than the minimum lock period and must not be longer than /// the maximum lock period. /// @param token token address to deposit /// @param amount amount of tokens to deposit /// @param lockPeriod lock period in seconds, 0 to not lock the deposit function deposit(address token, uint96 amount, uint32 lockPeriod) external { _depositFor(msg.sender, msg.sender, token, amount, lockPeriod); } /// @notice Deposit and optionally lock tokens to the contract on behalf of /// someone else. /// @dev Lock period will be normalized to weeks. If non-zero, it must not /// be shorter than the minimum lock period and must not be longer than /// the maximum lock period. /// @param depositOwner address that will be able to withdraw the deposit /// @param token token address to deposit /// @param amount amount of tokens to deposit /// @param lockPeriod lock period in seconds, 0 to not lock the deposit function depositFor( address depositOwner, address token, uint96 amount, uint32 lockPeriod ) external { _depositFor(depositOwner, msg.sender, token, amount, lockPeriod); } /// @notice Lock existing deposit for a given period. This function can be /// used to lock a deposit that was not locked when it was created or /// to extend the lock period of an already locked deposit. /// @param token address of the deposited token /// @param depositId id of the deposit /// @param lockPeriod new lock period in seconds function lock( address token, uint256 depositId, uint32 lockPeriod ) external { DepositInfo storage depositInfo = deposits[msg.sender][token][ depositId ]; if (depositInfo.balance == 0) { revert DepositNotFound(); } TokenAbility ability = tokenAbility[token]; if (ability != TokenAbility.DepositAndLock) { revert InsufficientTokenAbility(token, ability); } uint32 existingUnlockAt = depositInfo.unlockAt; uint32 newUnlockAt = _calculateUnlockTime( msg.sender, token, depositId, lockPeriod ); if (newUnlockAt <= existingUnlockAt) { revert LockPeriodTooShort( lockPeriod, newUnlockAt, existingUnlockAt ); } depositInfo.unlockAt = newUnlockAt; } /// @notice External withdraw function to enable the liquidity treasury to /// withdraw tokens from the contract. Only the liquidity treasury /// can withdraw tokens from the contract using this function. /// @param token token address to withdraw /// @param amount amount of respective token to withdraw function withdrawAsLiquidityTreasury( address token, uint256 amount ) external { if (msg.sender != liquidityTreasury) { revert SenderNotLiquidityTreasury(msg.sender); } if (amount == 0) { revert IncorrectAmount(amount); } if (!liquidityTreasuryManaged[token]) { revert AssetNotManagedByLiquidityTreasury(token); } emit WithdrawnByLiquidityTreasury(token, amount); IERC20(token).safeTransfer(msg.sender, amount); } /// @notice Mint a deposit receipt token against an existing deposit. /// @param token the token address related with the deposit /// @param depositId the ID of the deposit /// @param amount amount of the receipt token to mint function mintReceipt( address token, uint256 depositId, uint256 amount ) external { FeeInfo storage fee = feeInfo[token]; if (fee.receiptToken == address(0)) { revert ReceiptMintingDisabled(); } if (amount == 0) { revert IncorrectAmount(amount); } DepositInfo storage depositInfo = deposits[msg.sender][token][ depositId ]; if (depositInfo.balance == 0) { revert DepositNotFound(); } _updateFee(depositInfo, token); // Normalize deposit balance to match receipt token decimals uint96 normalizedBalance = _adjustTokenDecimals( token, fee.receiptToken, depositInfo.balance ); uint96 mintLimit = (normalizedBalance * fee.mintCap) / 100; if ( amount + depositInfo.receiptMinted + depositInfo.feeOwed > mintLimit ) { revert ReceiptMintLimitExceeded( mintLimit, depositInfo.receiptMinted, depositInfo.feeOwed, amount ); } depositInfo.receiptMinted += uint96(amount); fee.totalMinted += uint96(amount); emit ReceiptMinted(msg.sender, token, depositId, amount); IReceiptToken(fee.receiptToken).mintReceipt(msg.sender, amount); } /// @notice Repay the deposit receipt token of a particular deposit. /// @param token the token address related with the deposit /// @param depositId the ID of the deposit /// @param amount how much of the token to repay, up to outstanding balance function repayReceipt( address token, uint256 depositId, uint256 amount ) public { FeeInfo storage fee = feeInfo[token]; if (fee.receiptToken == address(0)) { revert ReceiptMintingDisabled(); } if (amount == 0) { revert IncorrectAmount(amount); } DepositInfo storage depositInfo = deposits[msg.sender][token][ depositId ]; if (depositInfo.balance == 0) { revert DepositNotFound(); } if (depositInfo.receiptMinted < amount) { revert RepayAmountExceededDebt(depositInfo.receiptMinted, amount); } _updateFee(depositInfo, token); depositInfo.receiptMinted -= uint96(amount); fee.totalMinted -= uint96(amount); emit ReceiptRepaid(msg.sender, token, depositId, amount); // transfer the repaid receipt tokens and burn them IERC20(fee.receiptToken).safeTransferFrom( msg.sender, address(this), amount ); IReceiptToken(fee.receiptToken).burnReceipt(amount); } /// @notice Request the to-tBTC migration for the given deposit. The /// migration happens asynchronously and is managed by the tBTC /// migration treasury. The contract gives no guarantees when the /// migration will be completed. /// @param token deposited token address /// @param depositId id of the deposit function requestTbtcMigration(address token, uint256 depositId) external { DepositInfo storage depositInfo = deposits[msg.sender][token][ depositId ]; TbtcMigrationInfo storage migrationInfo = tbtcMigrations[token]; if (!migrationInfo.isAllowed) { revert TbtcMigrationNotAllowed(); } if (depositInfo.tbtcMigrationState != TbtcMigrationState.NotRequested) { revert UnexpectedTbtcMigrationState( depositId, depositInfo.tbtcMigrationState, TbtcMigrationState.NotRequested ); } depositInfo.tbtcMigrationState = TbtcMigrationState.Requested; emit TbtcMigrationRequested(msg.sender, token, depositId); } /// @notice Used by the tBTC migration treasury to withdraw tokens from the /// deposits marked by their owners as requested for to-tBTC /// migration. The caller specifies which deposits are to begin the /// migration. Calling this function for the deposit marks it /// as to-tBTC migration started but gives no promise as when the /// to-tBTC migration will be completed. For all deposits for which /// the to-tBTC migration was started, the tBTC migration multisig /// must ensure to call the `finalizeTbtcMigration` function once /// the migration is completed. /// @param token deposited token address /// @param depositsToMigrate an array of deposits for which the to-tBTC /// migration was requested and from which the assets should be /// withdrawn for the to-tBTC migration function withdrawForTbtcMigration( address token, DepositToMigrate[] memory depositsToMigrate ) external { if (msg.sender != tbtcMigrationTreasury) { revert SenderNotTbtcMigrationTreasury(); } uint96 totalToMigrate = 0; for (uint256 i = 0; i < depositsToMigrate.length; i++) { address depositOwner = depositsToMigrate[i].depositor; uint256 depositId = depositsToMigrate[i].depositId; DepositInfo storage depositInfo = deposits[depositOwner][token][ depositId ]; if (depositInfo.balance == 0) { revert DepositNotFound(); } if ( depositInfo.tbtcMigrationState != TbtcMigrationState.Requested ) { revert UnexpectedTbtcMigrationState( depositId, depositInfo.tbtcMigrationState, TbtcMigrationState.Requested ); } depositInfo.tbtcMigrationState = TbtcMigrationState.InProgress; totalToMigrate += depositInfo.balance; emit TbtcMigrationStarted(depositOwner, token, depositId); } TbtcMigrationInfo storage migrationInfo = tbtcMigrations[token]; migrationInfo.totalMigrating += totalToMigrate; emit WithdrawnForTbtcMigration(token, totalToMigrate); IERC20(token).safeTransfer(tbtcMigrationTreasury, totalToMigrate); } /// @notice Used by the tBTC migration treasury to complete the to-tBTC /// migration for the selected set of deposits. For all of those /// deposits the migration should be started by calling /// `withdrawForTbtcMigration` function before. The treasury has to /// approve the amount of tBTC to the Portal contract equal to the /// total migrated amount of the migrated deposits, adjusted to tBTC /// token decimals. /// @param token pre-migration deposited token address /// @param migratedDeposits an array of deposits for which the to-tBTC /// migration was completed function completeTbtcMigration( address token, DepositToMigrate[] memory migratedDeposits ) external { if (msg.sender != tbtcMigrationTreasury) { revert SenderNotTbtcMigrationTreasury(); } uint96 totalMigrated = 0; for (uint256 i = 0; i < migratedDeposits.length; i++) { address depositOwner = migratedDeposits[i].depositor; uint256 depositId = migratedDeposits[i].depositId; DepositInfo storage depositInfo = deposits[depositOwner][token][ depositId ]; if ( depositInfo.tbtcMigrationState != TbtcMigrationState.InProgress ) { revert UnexpectedTbtcMigrationState( depositId, depositInfo.tbtcMigrationState, TbtcMigrationState.InProgress ); } totalMigrated += depositInfo.balance; depositInfo.tbtcMigrationState = TbtcMigrationState.Completed; emit TbtcMigrationCompleted(depositOwner, token, depositId); } TbtcMigrationInfo storage migrationInfo = tbtcMigrations[token]; migrationInfo.totalMigrating -= totalMigrated; uint96 totalMigratedInTbtc = _adjustTokenDecimals( token, tbtcToken, totalMigrated ); emit FundedFromTbtcMigration(totalMigratedInTbtc); IERC20(tbtcToken).safeTransferFrom( msg.sender, address(this), totalMigratedInTbtc ); } /// @notice Get the balance and unlock time of a given deposit. Note that /// the deposit could be migrated to tBTC so even though the `token` /// key under which it is available could still point to the /// pre-migration version, the witdrawal - when requested - will /// happen in tBTC. /// @param depositor depositor address /// @param token token address to get the balance /// @param depositId id of the deposit function getDeposit( address depositor, address token, uint256 depositId ) external view returns (DepositInfo memory) { return deposits[depositor][token][depositId]; } /// @notice Internal deposit function to deposit tokens on behalf of a given /// address - sender or someone else. If the lock period passed as /// a parameter is non-zero, the function will lock the deposit. /// @param depositOwner address that will be able to withdraw the deposit /// @param token token address to deposit /// @param amount amount of tokens to deposit /// @param lockPeriod lock period in seconds, 0 to not lock the deposit function _depositFor( address depositOwner, address depositFunder, address token, uint96 amount, uint32 lockPeriod ) internal { TokenAbility ability = tokenAbility[token]; if (ability == TokenAbility.None) { revert TokenNotSupported(token); } if (lockPeriod > 0 && ability == TokenAbility.Deposit) { revert InsufficientTokenAbility(token, ability); } if (depositOwner == address(0)) { revert IncorrectDepositor(depositOwner); } if (amount == 0) { revert IncorrectAmount(amount); } depositCount++; uint256 depositId = depositCount; emit Deposited(depositOwner, token, depositId, amount); deposits[depositOwner][token][depositId] = DepositInfo({ balance: amount, // solhint-disable-next-line not-rely-on-time unlockAt: _calculateUnlockTime( depositOwner, token, depositId, lockPeriod ), receiptMinted: 0, feeOwed: 0, lastFeeIntegral: 0, tbtcMigrationState: TbtcMigrationState.NotRequested }); IERC20(token).safeTransferFrom(depositFunder, address(this), amount); } /// @notice Calculates the unlock time normalizing the lock time value and /// making sure it is in the allowed range. Returns the timestamp at /// which the deposit can be unlocked. /// @dev This function DOES NOT check if the deposit exists and if the /// existing deposit already has a lock time. Please validate it before /// calling this function. /// @param depositor depositor address /// @param token token of the deposit that will be locked /// @param depositId id of the deposit that will be locked /// @param lockPeriod lock period in seconds function _calculateUnlockTime( address depositor, address token, uint depositId, uint32 lockPeriod ) internal returns (uint32) { // Short-circuit if there is no intention to lock. There is no need to // check the minimum/maximum lock time or normalize the lock period. if (lockPeriod == 0) { // solhint-disable-next-line not-rely-on-time return uint32(block.timestamp); } uint32 normalizedLockPeriod = _normalizeLockPeriod(lockPeriod); if ( normalizedLockPeriod == 0 || normalizedLockPeriod < minLockPeriod || normalizedLockPeriod > maxLockPeriod ) { revert LockPeriodOutOfRange(lockPeriod); } // solhint-disable-next-line not-rely-on-time uint32 unlockAt = uint32(block.timestamp) + normalizedLockPeriod; // It is gas-cheaper to pass the required parameters and emit the event // here instead of checking the result and emit the event in the calling // function. Keep in mind internal functions are inlined. emit Locked( depositor, token, depositId, unlockAt, normalizedLockPeriod ); return unlockAt; } /// @notice Internal function to update the fee information of a given /// deposit. /// @param depositInfo Storage reference to the deposit info to be updated. /// @param token Address of the token related to the deposit. function _updateFee( DepositInfo storage depositInfo, address token ) internal { _updateFeeIntegral(token); FeeInfo memory fee = feeInfo[token]; depositInfo.feeOwed += _calculateFeeAccrued( fee.feeIntegral - depositInfo.lastFeeIntegral, depositInfo.receiptMinted ); depositInfo.lastFeeIntegral = fee.feeIntegral; } /// @notice Internal function to update the fee integral for a given token. /// @param token Address of the token to update the fee integral. function _updateFeeIntegral(address token) internal { FeeInfo storage i = feeInfo[token]; uint96 feePerSecond = _calculateFeeRate(token); // solhint-disable-next-line not-rely-on-time uint32 timeInterval = uint32(block.timestamp) - i.lastFeeUpdateAt; uint96 accumulated = timeInterval * feePerSecond; i.feeIntegral += uint88(accumulated); // solhint-disable-next-line not-rely-on-time i.lastFeeUpdateAt = uint32(block.timestamp); } /// @notice Internal function to calculate the fee rate per second for a /// given token. /// @param token Address of the token. function _calculateFeeRate(address token) internal view returns (uint96) { FeeInfo memory i = feeInfo[token]; // The i.annualFee is expressed in % so we divide by 100 to get the // fee rate in token units: 10^18 / 10^2 = 10^16 return (uint96(i.annualFee) * (10 ** 16)) / (365 days); } /// @notice Calculates the fee accrued based on the given integral /// difference and minted amount. /// @dev Multiplies the integral difference by the minted amount and scales /// it down to fit the desired precision. /// @param integralDiff The difference in integral values. /// @param mintedAmount The amount of tokens minted. function _calculateFeeAccrued( uint88 integralDiff, uint96 mintedAmount ) internal pure returns (uint96) { return uint96((uint256(integralDiff) * uint256(mintedAmount)) / 10 ** 18); } /// @notice Normalizes the lock period to weeks. Will round down if not /// normalized. /// @param lockPeriod the lock period to be normalized function _normalizeLockPeriod( uint32 lockPeriod ) internal pure returns (uint32) { // slither-disable-next-line divide-before-multiply return (lockPeriod / 1 weeks) * 1 weeks; } /// @notice Adjusts the decimals amount to desired precision for the two /// token addresses and amount expressed in the precision used by /// one of them. /// @param sourceToken the token address of the source amount /// @param targetToken the token address of the target amount /// @param amount the source amount to be adjusted function _adjustTokenDecimals( address sourceToken, address targetToken, uint96 amount ) internal view returns (uint96) { uint8 sourceDecimals = IERC20WithDecimals(sourceToken).decimals(); uint8 targetDecimals = IERC20WithDecimals(targetToken).decimals(); if (sourceDecimals < targetDecimals) { return uint96(amount * (10 ** (targetDecimals - sourceDecimals))); } else if (sourceDecimals > targetDecimals) { return uint96(amount / (10 ** (sourceDecimals - targetDecimals))); } return amount; } }