diff --git a/.env.example b/.env.example index d4dd0284..2aa46d72 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,9 @@ VEDA_ADAPTER_OWNER_ADDRESS= VEDA_BORING_VAULT_ADDRESS= VEDA_TELLER_ADDRESS= VEDA_DEPOSIT_TOKEN_ADDRESS= +VAULT_MIGRATION_HELPER_OWNER_ADDRESS= +BASE_VEDA_ADAPTER_ADDRESS= +PREMIUM_VEDA_ADAPTER_ADDRESS= # Required for verifying contracts ETHERSCAN_API_KEY= diff --git a/script/DeployVaultMigrationHelper.s.sol b/script/DeployVaultMigrationHelper.s.sol new file mode 100644 index 00000000..17c5cc27 --- /dev/null +++ b/script/DeployVaultMigrationHelper.s.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import "forge-std/Script.sol"; +import { console2 } from "forge-std/console2.sol"; + +import { VaultMigrationHelper } from "../src/helpers/VaultMigrationHelper.sol"; + +/** + * @title DeployVaultMigrationHelper + * @notice Deploys VaultMigrationHelper deterministically with CREATE2. + * @dev Fill the required variables in the .env file + * @dev run the script with: + * forge script script/DeployVaultMigrationHelper.s.sol --rpc-url --private-key $PRIVATE_KEY --broadcast + */ +contract DeployVaultMigrationHelper is Script { + bytes32 internal salt; + address internal owner; + address internal baseAdapter; + address internal premiumAdapter; + + function setUp() public { + salt = bytes32(abi.encodePacked(vm.envString("SALT"))); + owner = vm.envAddress("VAULT_MIGRATION_HELPER_OWNER_ADDRESS"); + baseAdapter = vm.envAddress("BASE_VEDA_ADAPTER_ADDRESS"); + premiumAdapter = vm.envAddress("PREMIUM_VEDA_ADAPTER_ADDRESS"); + + console2.log("~~~"); + console2.log("Owner: %s", owner); + console2.log("BaseAdapter: %s", baseAdapter); + console2.log("PremiumAdapter: %s", premiumAdapter); + console2.log("Salt:"); + console2.logBytes32(salt); + } + + function run() public { + vm.startBroadcast(); + address deployed = address(new VaultMigrationHelper{ salt: salt }(owner, baseAdapter, premiumAdapter)); + vm.stopBroadcast(); + + console2.log("VaultMigrationHelper: %s", deployed); + } +} diff --git a/src/helpers/VaultMigrationHelper.sol b/src/helpers/VaultMigrationHelper.sol new file mode 100644 index 00000000..46f6ac7c --- /dev/null +++ b/src/helpers/VaultMigrationHelper.sol @@ -0,0 +1,510 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { Ownable2Step, Ownable } from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { MessageHashUtils } from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import { SignatureChecker } from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import { ModeLib } from "@erc7579/lib/ModeLib.sol"; +import { ExecutionLib } from "@erc7579/lib/ExecutionLib.sol"; + +import { VedaAdapter } from "./VedaAdapter.sol"; +import { ComplianceVedaAdapter } from "./ComplianceVedaAdapter.sol"; +import { IComplianceVedaTeller } from "./interfaces/IComplianceVedaTeller.sol"; +import { IRolesAuthority } from "./interfaces/IRolesAuthority.sol"; +import { Delegation, ModeCode } from "../utils/Types.sol"; + +/** + * @title VaultMigrationHelper + * @notice Atomically composes the existing base and premium Veda adapters, and moves premium shares with + * dual destination control (user + compliance backend). + * @dev Vault-to-vault migrations keep delegations targeted at the adapter that redeems them. Premium share + * transfers are redeemed by this helper. The user's root must: + * - pin the redeemed call with `AllowedCalldataEnforcer` terms + * `abi.encodePacked(uint256(0), IERC20.transfer.selector, abi.encode(address(this)))`, so the call is + * `transfer` to this helper and the amount stays unset on the root; + * - name the premium vault in `AllowedTargetsEnforcer`; + * - name this contract in `RedeemerEnforcer`. + * The root does not use `ERC20TransferAmountEnforcer`. This helper must hold `transferAllowedRole` on the + * premium Teller so Veda allows the first hop (helper is `to`) and the second hop (helper is `from`/`operator`). + * + * Destination control for `premiumTransfer` is dual, not backend-only and not user-only: + * - The share owner (`from`) picks the recipient and ERC-1271-signs the destination digest below. + * That locks `to` to the user's root delegation so a caller or backend cannot redirect the second hop. + * - The compliance backend still EIP-191-signs a digest that includes the same `to`, proving the + * recipient is premium-enabled / KYC'd. A user cannot send to an unapproved address even if they + * signed it. + * Neither party can complete a transfer to an address the other did not attest. The amount is the + * owner's full premium balance at execution (`balanceOf`). It is not read from a leaf caveat. + * The compliance digest includes that amount, so the backend still attests the quantity moved. + * First hop stays helper-only via the root caveats above. + * + * Signing schemes — wallets and CHOMP must implement both. They are not interchangeable: + * 1. Delegation chain (root + leaf): EIP-712 typed data through DelegationManager + * (`toTypedDataHash(domainHash, EncoderLib._getDelegationHash(delegation))`). The share owner + * signs the root; CHOMP (root `delegate`) signs the leaf. + * 2. Destination proof (`userSignature`): EIP-191 `personal_sign`, not EIP-712. Inner hash: + * `keccak256(abi.encode(address(this), block.chainid, from, to, delegations[length - 1].signature))`. + * Signed payload: `keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", inner))`. + * Verified with ERC-1271 on `from` via SignatureChecker. The chain is leaf-to-root, so + * `delegations[length - 1].signature` is the root the share owner signed (the initial transfer + * validation), not the leaf redelegation. Binding that signature ties `to` to this root without + * hashing the full delegation. + * 3. Compliance (`compliance.signature`): also EIP-191 `personal_sign`. Inner hash: + * `keccak256(abi.encode(address(this), teller, block.chainid, from, to, vault, amount, deadline))`. + * Same prefix as (2) and as Teller deposits, but the inner tuple starts with this helper so a + * Teller deposit signature cannot be reused. + */ +contract VaultMigrationHelper is Ownable2Step { + using SafeERC20 for IERC20; + + /** + * @notice Parameters for one base-to-premium migration. + * @param withdrawalDelegations Delegation chain redeemed by the base adapter to withdraw base vault shares. + * @param minimumAssets Minimum underlying assets that the base withdrawal must return. + * @param depositDelegations Delegation chain redeemed by the premium adapter to deposit the withdrawn assets. + * @param minimumMint Minimum premium vault shares that the destination deposit must mint. + * @param compliance Compliance authorization forwarded unchanged to the premium adapter. + */ + struct ToPremiumParams { + Delegation[] withdrawalDelegations; + uint256 minimumAssets; + Delegation[] depositDelegations; + uint256 minimumMint; + IComplianceVedaTeller.ComplianceData compliance; + } + + /** + * @notice Parameters for one premium-to-base migration. + * @param withdrawalDelegations Delegation chain redeemed by the premium adapter to withdraw premium shares. + * @param minimumAssets Minimum underlying assets that the premium withdrawal must return. + * @param depositDelegations Delegation chain redeemed by the base adapter to deposit the withdrawn assets. + * @param minimumMint Minimum base vault shares that the destination deposit must mint. + */ + struct ToBaseParams { + Delegation[] withdrawalDelegations; + uint256 minimumAssets; + Delegation[] depositDelegations; + uint256 minimumMint; + } + + /** + * @notice Parameters for one dual-signed premium share transfer. + * @param from Share owner and root delegator. + * @param to Recipient chosen by `from` and attested as premium-enabled by the compliance backend. + * @param delegations Chain sorted leaf to root. Leaf `delegate` must be this helper. + * @param compliance Backend-issued EIP-191 approval (`deadline` + `signature`) that includes `to`. + * @param userSignature EIP-191 `personal_sign` from `from` (not EIP-712). Exact inner hash is documented on the contract. + */ + struct PremiumTransferParams { + address from; + address to; + Delegation[] delegations; + IComplianceVedaTeller.ComplianceData compliance; + bytes userSignature; + } + + ////////////////////////////// Events ////////////////////////////// + + /** + * @notice Emitted after a base-to-premium migration completes. + * @param delegator Root delegator whose vault position was migrated. + * @param sourceShares Base vault shares withdrawn. + * @param destShares Premium vault shares minted. + */ + event MigrationToPremiumExecuted(address indexed delegator, uint256 sourceShares, uint256 destShares); + + /** + * @notice Emitted after a premium-to-base migration completes. + * @param delegator Root delegator whose vault position was migrated. + * @param sourceShares Premium vault shares withdrawn. + * @param destShares Base vault shares minted. + */ + event MigrationToBaseExecuted(address indexed delegator, uint256 sourceShares, uint256 destShares); + + /** + * @notice Emitted after a batch of base-to-premium migrations completes. + * @param caller Address that submitted the permissionless batch. + * @param count Number of migrations executed. + */ + event BatchMigrationToPremiumExecuted(address indexed caller, uint256 count); + + /** + * @notice Emitted after a batch of premium-to-base migrations completes. + * @param caller Address that submitted the permissionless batch. + * @param count Number of migrations executed. + */ + event BatchMigrationToBaseExecuted(address indexed caller, uint256 count); + + /** + * @notice Emitted after a dual-signed premium share transfer. + * @param from Share owner whose full premium balance was moved. + * @param to Recipient chosen by `from` and attested as premium-enabled by the compliance backend. + * @param amount Number of premium vault shares transferred. + */ + event PremiumTransferExecuted(address indexed from, address indexed to, uint256 amount); + + /** + * @notice Emitted after a batch of premium share transfers completes. + * @param caller Address that submitted the permissionless batch. + * @param count Number of transfers executed. + */ + event BatchPremiumTransferExecuted(address indexed caller, uint256 count); + + /** + * @notice Emitted when stuck tokens are withdrawn by owner. + * @param token Address of the token withdrawn. + * @param recipient Address of the recipient. + * @param amount Amount of tokens withdrawn. + */ + event StuckTokensWithdrawn(IERC20 indexed token, address indexed recipient, uint256 amount); + + ////////////////////////////// Errors ////////////////////////////// + + /// @dev Thrown when a required constructor address is zero. + error InvalidZeroAddress(); + + /// @dev Thrown when a zero address is provided for the recipient. + error InvalidRecipient(); + + /// @dev Thrown when either delegation chain contains fewer than two delegations. + error InvalidDelegationsLength(); + + /// @dev Thrown when a batch is empty. + error InvalidBatchLength(); + + /// @dev Thrown when the withdrawal and deposit chains have different root delegators. + error DelegatorMismatch(); + + /// @dev Thrown when compliance signature verification fails. + error ComplianceCheckFailed(); + + /// @dev Thrown when the premium Teller has compliance signer checks disabled (`complianceSignerRole == 255`). + error ComplianceDisabled(); + + /// @dev Thrown when the share owner's destination signature is missing or does not match from, to, and leaf. + error InvalidUserSignature(); + + ////////////////////////////// State ////////////////////////////// + + /** + * @notice Base vault adapter used for base deposits and withdrawals. + */ + VedaAdapter public immutable baseAdapter; + + /** + * @notice Premium compliance-gated adapter used for premium deposits and withdrawals. + */ + ComplianceVedaAdapter public immutable premiumAdapter; + + /** + * @notice Compliance message hashes already consumed by `premiumTransfer`. + */ + mapping(bytes32 messageHash => bool used) public usedComplianceSignatures; + + ////////////////////////////// Constructor ////////////////////////////// + + /** + * @notice Initializes the helper with its owner and the base and premium Veda adapters. + * @param _owner Address of the contract owner. + * @param _baseAdapter Address of the base Veda adapter. + * @param _premiumAdapter Address of the premium compliance-gated Veda adapter. + */ + constructor(address _owner, address _baseAdapter, address _premiumAdapter) Ownable(_owner) { + if (_baseAdapter == address(0) || _premiumAdapter == address(0)) { + revert InvalidZeroAddress(); + } + baseAdapter = VedaAdapter(_baseAdapter); + premiumAdapter = ComplianceVedaAdapter(_premiumAdapter); + } + + ////////////////////////////// External Methods ////////////////////////////// + + /** + * @notice Migrates one delegator from the base vault to the premium vault. + * @dev The withdrawal chain is redeemed by `baseAdapter`; the deposit chain and compliance data are + * forwarded to `premiumAdapter`. + * @param _params Withdrawal, deposit, slippage, and compliance parameters for the migration. + * @notice Security consideration: Callable by anyone. Security remains enforced by the delegation chains, + * their caveats, and the premium Teller's compliance validation. + */ + function migrateToPremiumByDelegation(ToPremiumParams calldata _params) external { + _migrateToPremiumByDelegation(_params); + } + + /** + * @notice Migrates multiple delegators from base to premium sequentially in one atomic transaction. + * @dev A revert from any withdrawal or deposit leg rolls back the entire batch. + * @param _params Migration parameters for each stream. + * @notice Security consideration: Callable by anyone. Every delegation chain remains independently enforced. + */ + function migrateToPremiumByDelegationBatch(ToPremiumParams[] calldata _params) external { + uint256 length_ = _params.length; + if (length_ == 0) revert InvalidBatchLength(); + + for (uint256 i = 0; i < length_;) { + _migrateToPremiumByDelegation(_params[i]); + unchecked { + ++i; + } + } + + emit BatchMigrationToPremiumExecuted(msg.sender, length_); + } + + /** + * @notice Migrates one delegator from the premium vault to the base vault. + * @dev The withdrawal chain is redeemed by `premiumAdapter`; the deposit chain is redeemed by `baseAdapter`. + * @param _params Withdrawal, deposit, and slippage parameters for the migration. + * @notice Security consideration: Callable by anyone. Security remains enforced by the delegation chains + * and their caveats. + */ + function migrateToBaseByDelegation(ToBaseParams calldata _params) external { + _migrateToBaseByDelegation(_params); + } + + /** + * @notice Migrates multiple delegators from premium to base sequentially in one atomic transaction. + * @dev A revert from any withdrawal or deposit leg rolls back the entire batch. + * @param _params Migration parameters for each stream. + * @notice Security consideration: Callable by anyone. Every delegation chain remains independently enforced. + */ + function migrateToBaseByDelegationBatch(ToBaseParams[] calldata _params) external { + uint256 length_ = _params.length; + if (length_ == 0) revert InvalidBatchLength(); + + for (uint256 i = 0; i < length_;) { + _migrateToBaseByDelegation(_params[i]); + unchecked { + ++i; + } + } + + emit BatchMigrationToBaseExecuted(msg.sender, length_); + } + + /** + * @notice Moves `from`'s entire premium vault balance to `to` after user and compliance checks. + * @dev The delegation chain must be redeemable only by this helper. The root pins `transfer` to this helper + * via `AllowedCalldataEnforcer` (selector plus recipient) and the premium vault via `AllowedTargetsEnforcer`. + * Production must grant this helper `transferAllowedRole` on the premium Teller. + * The amount is `balanceOf(from)` at execution, not a value encoded in the leaf. + * @param _params From, to, chain, compliance, and user destination signature. + * @notice Security consideration: Callable by anyone. Destination is dual-controlled: the user signature + * binds `to` to this helper, chain, `from`, and the root delegation the user signed; the compliance + * signature still includes `to` and the full balance so only a premium-enabled recipient can receive + * that exact amount. Delegations are EIP-712; `userSignature` and compliance are EIP-191 `personal_sign` + * over the exact inner hashes documented on the contract. + * A Teller deposit signature cannot be reused because it binds the Teller address, not this helper. + * Reverts when Teller compliance is disabled. + */ + function premiumTransfer(PremiumTransferParams calldata _params) external { + _premiumTransfer(_params); + } + + /** + * @notice Moves multiple delegators' entire premium vault balances sequentially in one atomic transaction. + * @dev A revert from any transfer rolls back the entire batch. Each stream needs its own user signature + * (destination) and compliance signature (premium-enabled `to`) because both digests include `from`/`to`. + * @param _params Transfer parameters for each stream. + * @notice Security consideration: Callable by anyone. Every delegation chain, user destination signature, + * and compliance signature remains independently enforced. + */ + function premiumTransferBatch(PremiumTransferParams[] calldata _params) external { + uint256 length_ = _params.length; + if (length_ == 0) revert InvalidBatchLength(); + + for (uint256 i = 0; i < length_;) { + _premiumTransfer(_params[i]); + unchecked { + ++i; + } + } + + emit BatchPremiumTransferExecuted(msg.sender, length_); + } + + /** + * @notice Emergency function to recover tokens accidentally sent to this contract. + * @dev Migrations should not leave balances here. `premiumTransfer` only holds shares mid-transaction. + * This function is only for recovering tokens sent to this contract by mistake. + * @param _token The token to be recovered. + * @param _amount The amount of tokens to recover. + * @param _recipient The address to receive the recovered tokens. + */ + function withdrawEmergency(IERC20 _token, uint256 _amount, address _recipient) external onlyOwner { + if (_recipient == address(0)) revert InvalidRecipient(); + + _token.safeTransfer(_recipient, _amount); + + emit StuckTokensWithdrawn(_token, _recipient, _amount); + } + + ////////////////////////////// Private/Internal Methods ////////////////////////////// + + /** + * @notice Executes one dual-signed premium share transfer. + * @dev User destination signature is checked before the backend compliance signature and before redeem. + */ + function _premiumTransfer(PremiumTransferParams calldata _params) internal { + address from_ = _params.from; + address to_ = _params.to; + if (from_ == address(0) || to_ == address(0)) revert InvalidZeroAddress(); + + uint256 length_ = _params.delegations.length; + if (length_ < 2) revert InvalidDelegationsLength(); + if (_params.delegations[length_ - 1].delegator != from_) revert DelegatorMismatch(); + + IERC20 vault_ = IERC20(premiumAdapter.boringVault()); + uint256 amount_ = vault_.balanceOf(from_); + + _verifyUserTransferSignature(from_, to_, _params.delegations[length_ - 1].signature, _params.userSignature); + _verifyTransferCompliance(from_, to_, address(vault_), amount_, _params.compliance); + + bytes[] memory permissionContexts_ = new bytes[](1); + permissionContexts_[0] = abi.encode(_params.delegations); + + ModeCode[] memory encodedModes_ = new ModeCode[](1); + encodedModes_[0] = ModeLib.encodeSimpleSingle(); + + bytes[] memory executionCallDatas_ = new bytes[](1); + executionCallDatas_[0] = + ExecutionLib.encodeSingle(address(vault_), 0, abi.encodeCall(IERC20.transfer, (address(this), amount_))); + + premiumAdapter.delegationManager().redeemDelegations(permissionContexts_, encodedModes_, executionCallDatas_); + vault_.safeTransfer(to_, amount_); + + emit PremiumTransferExecuted(from_, to_, amount_); + } + + /** + * @notice Executes one base-to-premium migration. + * @param _params Migration parameters forwarded to the base and premium adapters. + */ + function _migrateToPremiumByDelegation(ToPremiumParams calldata _params) internal { + address delegator_ = _validateDelegators(_params.withdrawalDelegations, _params.depositDelegations); + IERC20 sourceVault_ = IERC20(baseAdapter.boringVault()); + IERC20 destVault_ = IERC20(premiumAdapter.boringVault()); + uint256 sourceBefore_ = sourceVault_.balanceOf(delegator_); + uint256 destBefore_ = destVault_.balanceOf(delegator_); + + baseAdapter.withdrawByDelegation(_params.withdrawalDelegations, _params.minimumAssets); + premiumAdapter.depositByDelegation(_params.depositDelegations, _params.minimumMint, _params.compliance); + + emit MigrationToPremiumExecuted( + delegator_, sourceBefore_ - sourceVault_.balanceOf(delegator_), destVault_.balanceOf(delegator_) - destBefore_ + ); + } + + /** + * @notice Executes one premium-to-base migration. + * @param _params Migration parameters forwarded to the premium and base adapters. + */ + function _migrateToBaseByDelegation(ToBaseParams calldata _params) internal { + address delegator_ = _validateDelegators(_params.withdrawalDelegations, _params.depositDelegations); + IERC20 sourceVault_ = IERC20(premiumAdapter.boringVault()); + IERC20 destVault_ = IERC20(baseAdapter.boringVault()); + uint256 sourceBefore_ = sourceVault_.balanceOf(delegator_); + uint256 destBefore_ = destVault_.balanceOf(delegator_); + + premiumAdapter.withdrawByDelegation(_params.withdrawalDelegations, _params.minimumAssets); + baseAdapter.depositByDelegation(_params.depositDelegations, _params.minimumMint); + + emit MigrationToBaseExecuted( + delegator_, sourceBefore_ - sourceVault_.balanceOf(delegator_), destVault_.balanceOf(delegator_) - destBefore_ + ); + } + + /** + * @notice Validates both delegation chains and returns their shared root delegator. + * @param _withdrawalDelegations Source-vault withdrawal chain sorted from leaf to root. + * @param _depositDelegations Destination-vault deposit chain sorted from leaf to root. + * @return delegator_ Shared root delegator that owns both sides of the migration. + */ + function _validateDelegators( + Delegation[] calldata _withdrawalDelegations, + Delegation[] calldata _depositDelegations + ) + private + pure + returns (address delegator_) + { + uint256 withdrawalLength_ = _withdrawalDelegations.length; + uint256 depositLength_ = _depositDelegations.length; + if (withdrawalLength_ < 2 || depositLength_ < 2) revert InvalidDelegationsLength(); + + delegator_ = _withdrawalDelegations[withdrawalLength_ - 1].delegator; + if (delegator_ != _depositDelegations[depositLength_ - 1].delegator) revert DelegatorMismatch(); + } + + /** + * @notice Verifies that `from` authorized this exact destination for their root delegation. + * @dev Destination proof is EIP-191 `personal_sign`, not the EIP-712 scheme used for delegations. + * Inner: `keccak256(abi.encode(address(this), block.chainid, from, to, root.signature))`. + * Signed: `toEthSignedMessageHash(inner)` == `"\x19Ethereum Signed Message:\n32" || inner`. + * Verified with ERC-1271 on `from` via SignatureChecker so a backend cannot swap `to` after + * the user signed. The chain is sorted leaf to root, so the bound signature is + * `delegations[length - 1].signature`: the user's initial transfer validation, not the + * redelegation at `delegations[0]`. Binding that signature ties `to` to this root without + * hashing the full delegation on-chain. + */ + function _verifyUserTransferSignature( + address _from, + address _to, + bytes calldata _rootSignature, + bytes calldata _userSignature + ) + private + view + { + bytes32 messageHash_ = keccak256(abi.encode(address(this), block.chainid, _from, _to, _rootSignature)); + bytes32 ethSignedHash_ = MessageHashUtils.toEthSignedMessageHash(messageHash_); + if (!SignatureChecker.isValidSignatureNow(_from, ethSignedHash_, _userSignature)) { + revert InvalidUserSignature(); + } + } + + /** + * @notice Verifies a transfer compliance signature using the Teller's signer role and window. + * @dev Destination is not user-only: this digest still includes `_to` so the backend attests that + * the recipient is premium-enabled / KYC'd. A user-signed `to` that the backend did not approve + * cannot receive. EIP-191 `personal_sign` (same prefix as `userSignature` and Teller deposits): + * inner `keccak256(abi.encode(address(this), teller, chainId, from, to, vault, amount, deadline))`. + * Starts with this helper so it cannot equal a Teller deposit hash. Reverts if the Teller has + * disabled compliance (`complianceSignerRole == 255`). + */ + function _verifyTransferCompliance( + address _from, + address _to, + address _vault, + uint256 _amount, + IComplianceVedaTeller.ComplianceData calldata _compliance + ) + private + { + IComplianceVedaTeller teller_ = premiumAdapter.teller(); + uint8 complianceSignerRole_ = teller_.complianceSignerRole(); + if (complianceSignerRole_ == type(uint8).max) revert ComplianceDisabled(); + + bytes32 messageHash_ = keccak256( + abi.encode(address(this), address(teller_), block.chainid, _from, _to, _vault, _amount, _compliance.deadline) + ); + if (usedComplianceSignatures[messageHash_]) revert ComplianceCheckFailed(); + if (block.timestamp > _compliance.deadline) revert ComplianceCheckFailed(); + + uint96 complianceWindow_ = teller_.complianceWindow(); + if (complianceWindow_ > 0 && _compliance.deadline > block.timestamp + complianceWindow_) { + revert ComplianceCheckFailed(); + } + + bytes32 ethSignedHash_ = MessageHashUtils.toEthSignedMessageHash(messageHash_); + address recovered_ = ECDSA.recover(ethSignedHash_, _compliance.signature); + if (!IRolesAuthority(teller_.authority()).doesUserHaveRole(recovered_, complianceSignerRole_)) { + revert ComplianceCheckFailed(); + } + + usedComplianceSignatures[messageHash_] = true; + } +} diff --git a/src/helpers/interfaces/IComplianceVedaTeller.sol b/src/helpers/interfaces/IComplianceVedaTeller.sol index b247f1e0..57df77a0 100644 --- a/src/helpers/interfaces/IComplianceVedaTeller.sol +++ b/src/helpers/interfaces/IComplianceVedaTeller.sol @@ -44,4 +44,20 @@ interface IComplianceVedaTeller { ) external returns (uint256 assetsOut); + + /** + * @notice RolesAuthority used to check compliance-signer and transfer-allowlist roles. + */ + function authority() external view returns (address); + + /** + * @notice Role ID that a recovered compliance signer must hold, or 255 to disable Teller checks. + * @dev `VaultMigrationHelper.premiumTransfer` reverts when this value is 255. + */ + function complianceSignerRole() external view returns (uint8); + + /** + * @notice Maximum seconds a compliance deadline may extend beyond `block.timestamp`, or 0 for no cap. + */ + function complianceWindow() external view returns (uint96); } diff --git a/src/helpers/interfaces/IRolesAuthority.sol b/src/helpers/interfaces/IRolesAuthority.sol new file mode 100644 index 00000000..d1511445 --- /dev/null +++ b/src/helpers/interfaces/IRolesAuthority.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +/** + * @title IRolesAuthority + * @notice Minimal interface for Solmate-style RolesAuthority role checks used by Veda tellers. + */ +interface IRolesAuthority { + function doesUserHaveRole(address user, uint8 role) external view returns (bool); +} diff --git a/test/helpers/VaultMigrationHelper.t.sol b/test/helpers/VaultMigrationHelper.t.sol new file mode 100644 index 00000000..d963f96b --- /dev/null +++ b/test/helpers/VaultMigrationHelper.t.sol @@ -0,0 +1,1185 @@ +// SPDX-License-Identifier: MIT AND Apache-2.0 +pragma solidity 0.8.23; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; + +import { EncoderLib } from "../../src/libraries/EncoderLib.sol"; +import { BasicERC20 } from "../utils/BasicERC20.t.sol"; +import { IComplianceVedaTeller } from "../../src/helpers/interfaces/IComplianceVedaTeller.sol"; +import { IVedaTeller } from "../../src/helpers/interfaces/IVedaTeller.sol"; +import { VedaAdapter } from "../../src/helpers/VedaAdapter.sol"; +import { ComplianceVedaAdapter } from "../../src/helpers/ComplianceVedaAdapter.sol"; +import { VaultMigrationHelper } from "../../src/helpers/VaultMigrationHelper.sol"; +import { ERC20TransferAmountEnforcer } from "../../src/enforcers/ERC20TransferAmountEnforcer.sol"; +import { RedeemerEnforcer } from "../../src/enforcers/RedeemerEnforcer.sol"; +import { AllowedCalldataEnforcer } from "../../src/enforcers/AllowedCalldataEnforcer.sol"; +import { AllowedTargetsEnforcer } from "../../src/enforcers/AllowedTargetsEnforcer.sol"; +import { Delegation, Caveat } from "../../src/utils/Types.sol"; +import { BaseTest } from "../utils/BaseTest.t.sol"; +import { Implementation, SignatureType, TestUser } from "../utils/Types.t.sol"; + +interface IRolesAuthority { + function owner() external view returns (address); + function setUserRole(address user, uint8 role, bool enabled) external; + function setRoleCapability(uint8 role, address target, bytes4 functionSig, bool enabled) external; + function doesUserHaveRole(address user, uint8 role) external view returns (bool); +} + +interface ITellerConfiguration { + function owner() external view returns (address); + function setTransferRestrictions(uint8 transferAllowedRole, uint8 allowlistedRouterRole) external; +} + +interface IVedaAccountant { + function decimals() external view returns (uint8); + function getRateInQuoteSafe(address quote) external view returns (uint256); +} + +/// forge-config: default.evm_version = "cancun" +contract VaultMigrationHelperTest is BaseTest { + IERC20 internal constant MUSD = IERC20(0xacA92E438df0B2401fF60dA7E4337B687a2435DA); + IERC20 internal constant BASE_VAULT = IERC20(0xb4563bcD3B7764CCBf497f515585f70B6C3EA5Ae); + IVedaTeller internal constant BASE_TELLER = IVedaTeller(0x2D49EA58A4C70b62c8B56DE971310d9e999c8117); + IVedaAccountant internal constant BASE_ACCOUNTANT = IVedaAccountant(0x7382c5b8B51B8C4f127B3123C1039581BAA5A06B); + address internal constant BASE_LENS = 0xA816ECd922de94c6879AD23B9A884dB257F20947; + address internal constant BUFFER_LENS = 0xf484d54AF87199F421967E2231Dd6dC348C7Ee6A; + address internal constant DEPLOYED_BASE_ADAPTER = 0xaD4c09d065fDb6320FA8ADf69460CDd9d472C25A; + + IComplianceVedaTeller internal constant PREMIUM_TELLER = IComplianceVedaTeller(0xB0025a2eBc0474d4F28E975F0D3E70471246ebae); + IERC20 internal constant PREMIUM_VAULT = IERC20(0xBFeC8c2b1ccea3931a1363E4CaC27352c1C908B7); + IVedaAccountant internal constant PREMIUM_ACCOUNTANT = IVedaAccountant(0xb4c9e25D3D6a4E42A7d52C21822D6016d29Ee0A2); + IRolesAuthority internal constant PREMIUM_ROLES_AUTHORITY = IRolesAuthority(0x00f0CF4f540D1d47470f565Ecb11a75E073c2dd0); + IRolesAuthority internal constant BASE_ROLES_AUTHORITY = IRolesAuthority(0x1eC540C9a4656a50F2D6DaaBd647753F97f082D1); + + address internal constant MUSD_WHALE = 0x6cb5094cfe45Ee97938702637478fe7146A8FA0f; + uint8 internal constant COMPLIANCE_SIGNER_ROLE = 36; + uint8 internal constant TRANSFER_ALLOWED_ROLE = 37; + uint8 internal constant TEST_ROUTER_ROLE = 254; + uint8 internal constant BASE_ADAPTER_ROLE = 90; + + uint256 internal constant INITIAL_MUSD_BALANCE = 100_000e6; + uint256 internal constant DEPOSIT_AMOUNT = 1_000e6; + uint256 internal constant SHARE_LOCK_SECONDS = 61; + uint256 internal constant COMPLIANCE_SIGNER_KEY = 0xC011A11CE; + + ERC20TransferAmountEnforcer internal erc20TransferAmountEnforcer; + RedeemerEnforcer internal redeemerEnforcer; + AllowedCalldataEnforcer internal allowedCalldataEnforcer; + AllowedTargetsEnforcer internal allowedTargetsEnforcer; + VedaAdapter internal baseAdapter; + ComplianceVedaAdapter internal premiumAdapter; + VaultMigrationHelper internal migrationHelper; + address internal adapterOwner; + address internal complianceSigner; + + event MigrationToPremiumExecuted(address indexed delegator, uint256 sourceShares, uint256 destShares); + event MigrationToBaseExecuted(address indexed delegator, uint256 sourceShares, uint256 destShares); + event BatchMigrationToPremiumExecuted(address indexed caller, uint256 count); + event BatchMigrationToBaseExecuted(address indexed caller, uint256 count); + event PremiumTransferExecuted(address indexed from, address indexed to, uint256 amount); + event BatchPremiumTransferExecuted(address indexed caller, uint256 count); + event StuckTokensWithdrawn(IERC20 indexed token, address indexed recipient, uint256 amount); + + function setUp() public override { + vm.createSelectFork(vm.envString("MONAD_RPC_URL")); + + IMPLEMENTATION = Implementation.Hybrid; + SIGNATURE_TYPE = SignatureType.RawP256; + super.setUp(); + + adapterOwner = makeAddr("VaultMigrationHelper Owner"); + complianceSigner = vm.addr(COMPLIANCE_SIGNER_KEY); + erc20TransferAmountEnforcer = new ERC20TransferAmountEnforcer(); + redeemerEnforcer = new RedeemerEnforcer(); + allowedCalldataEnforcer = new AllowedCalldataEnforcer(); + allowedTargetsEnforcer = new AllowedTargetsEnforcer(); + + baseAdapter = + new VedaAdapter(adapterOwner, address(delegationManager), address(BASE_VAULT), address(BASE_TELLER), address(MUSD)); + premiumAdapter = new ComplianceVedaAdapter( + adapterOwner, address(delegationManager), address(PREMIUM_VAULT), address(PREMIUM_TELLER), address(MUSD) + ); + migrationHelper = new VaultMigrationHelper(adapterOwner, address(baseAdapter), address(premiumAdapter)); + + _configureBaseAdapterRole(); + _configurePremiumForkRoles(); + _fundUser(users.alice); + _fundUser(users.carol); + + vm.label(address(MUSD), "mUSD"); + vm.label(address(BASE_VAULT), "Base mUSD Vault"); + vm.label(address(PREMIUM_VAULT), "Premium mUSD Vault"); + vm.label(address(BASE_TELLER), "Base Veda Teller"); + vm.label(address(PREMIUM_TELLER), "Premium Veda Teller"); + vm.label(address(BASE_ACCOUNTANT), "Base Accountant"); + vm.label(address(PREMIUM_ACCOUNTANT), "Premium Accountant"); + vm.label(BASE_LENS, "Base Lens"); + vm.label(BUFFER_LENS, "Buffer Lens"); + vm.label(DEPLOYED_BASE_ADAPTER, "Deployed Base Adapter"); + vm.label(address(baseAdapter), "VedaAdapter"); + vm.label(address(premiumAdapter), "ComplianceVedaAdapter"); + vm.label(address(migrationHelper), "VaultMigrationHelper"); + vm.label(complianceSigner, "Compliance Signer"); + } + + function test_constructor_wiresAdaptersAndRevertsOnZeroAddresses() public { + assertEq(address(migrationHelper.baseAdapter()), address(baseAdapter)); + assertEq(address(migrationHelper.premiumAdapter()), address(premiumAdapter)); + assertEq(migrationHelper.owner(), adapterOwner); + + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableInvalidOwner.selector, address(0))); + new VaultMigrationHelper(address(0), address(baseAdapter), address(premiumAdapter)); + + vm.expectRevert(VaultMigrationHelper.InvalidZeroAddress.selector); + new VaultMigrationHelper(adapterOwner, address(0), address(premiumAdapter)); + + vm.expectRevert(VaultMigrationHelper.InvalidZeroAddress.selector); + new VaultMigrationHelper(adapterOwner, address(baseAdapter), address(0)); + } + + function test_lifecycle_depositAndWithdrawBaseThenPremium() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 0); + assertGt(baseShares_, 0); + assertEq(MUSD.balanceOf(address(users.alice.deleGator)), INITIAL_MUSD_BALANCE - DEPOSIT_AMOUNT); + _assertNoAdapterDust(); + + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 assetsOut_ = _withdrawFromBase(users.alice, baseShares_, 1); + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertApproxEqAbs(assetsOut_, DEPOSIT_AMOUNT, DEPOSIT_AMOUNT / 100); + _assertNoAdapterDust(); + + uint256 premiumShares_ = _depositToPremium(users.alice, assetsOut_, 2, block.timestamp + 30 minutes); + assertGt(premiumShares_, 0); + assertEq(MUSD.balanceOf(address(users.alice.deleGator)), INITIAL_MUSD_BALANCE - DEPOSIT_AMOUNT); + _assertNoAdapterDust(); + + uint256 musdBeforePremiumWithdraw_ = MUSD.balanceOf(address(users.alice.deleGator)); + _withdrawFromPremium(users.alice, premiumShares_, 3); + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(MUSD.balanceOf(address(users.alice.deleGator)), musdBeforePremiumWithdraw_); + _assertNoAdapterDust(); + } + + function test_migrateToPremium_movesSharesAndLeavesNoDust() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 10); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + uint256 musdBefore_ = MUSD.balanceOf(address(users.alice.deleGator)); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 11, quotedAssets_, 12, block.timestamp + 30 minutes); + + vm.expectEmit(true, false, false, false, address(migrationHelper)); + emit MigrationToPremiumExecuted(address(users.alice.deleGator), 0, 0); + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(MUSD.balanceOf(address(users.alice.deleGator)), musdBefore_); + _assertNoAdapterDust(); + } + + function test_migrateToBase_movesSharesAndLeavesNoDust() public { + uint256 premiumShares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 20, block.timestamp + 30 minutes); + uint256 quotedAssets_ = _quotePremiumAssets(premiumShares_); + uint256 musdBefore_ = MUSD.balanceOf(address(users.alice.deleGator)); + VaultMigrationHelper.ToBaseParams memory params_ = _toBaseParams(users.alice, premiumShares_, 21, quotedAssets_, 22); + + vm.expectEmit(true, false, false, false, address(migrationHelper)); + emit MigrationToBaseExecuted(address(users.alice.deleGator), 0, 0); + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToBaseByDelegation(params_); + + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(MUSD.balanceOf(address(users.alice.deleGator)), musdBefore_); + _assertNoAdapterDust(); + } + + function test_migrateToPremium_isPermissionless() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 30); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + + vm.prank(address(users.carol.deleGator)); + migrationHelper.migrateToPremiumByDelegation( + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 31, quotedAssets_, 32, block.timestamp + 30 minutes) + ); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(PREMIUM_VAULT.balanceOf(address(users.carol.deleGator)), 0); + _assertNoAdapterDust(); + } + + function test_migrateToPremium_revertsBothLegsWhenDepositFails() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 40); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + uint256 musdBefore_ = MUSD.balanceOf(address(users.alice.deleGator)); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 41, quotedAssets_, 42, block.timestamp + 30 minutes); + params_.minimumMint = type(uint256).max; + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), baseShares_); + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(MUSD.balanceOf(address(users.alice.deleGator)), musdBefore_); + _assertNoAdapterDust(); + } + + function test_migrateToBase_revertsOnDelegatorMismatch() public { + uint256 aliceShares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 50, block.timestamp + 30 minutes); + uint256 quotedAssets_ = _quotePremiumAssets(aliceShares_); + + VaultMigrationHelper.ToBaseParams memory params_ = VaultMigrationHelper.ToBaseParams({ + withdrawalDelegations: _createDelegationChain( + users.alice, address(premiumAdapter), address(PREMIUM_VAULT), aliceShares_, 51 + ), + minimumAssets: 0, + depositDelegations: _createDelegationChain(users.carol, address(baseAdapter), address(MUSD), quotedAssets_, 52), + minimumMint: 0 + }); + + vm.expectRevert(VaultMigrationHelper.DelegatorMismatch.selector); + migrationHelper.migrateToBaseByDelegation(params_); + + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), aliceShares_); + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(BASE_VAULT.balanceOf(address(users.carol.deleGator)), 0); + } + + function test_migrateToPremium_revertsOnShortDelegationChain() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 60); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 61, quotedAssets_, 62, block.timestamp + 30 minutes); + params_.withdrawalDelegations = new Delegation[](1); + + vm.expectRevert(VaultMigrationHelper.InvalidDelegationsLength.selector); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), baseShares_); + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + } + + function test_migrateToPremiumBatch_movesExactAmountsForEachDelegator() public { + uint256 aliceAmount_ = 300e6; + uint256 carolAmount_ = 400e6; + uint256 aliceShares_ = _depositToBase(users.alice, aliceAmount_, 70); + uint256 carolShares_ = _depositToBase(users.carol, carolAmount_, 71); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceAssets_ = _quoteBaseAssets(aliceShares_); + uint256 carolAssets_ = _quoteBaseAssets(carolShares_); + VaultMigrationHelper.ToPremiumParams[] memory params_ = new VaultMigrationHelper.ToPremiumParams[](2); + params_[0] = _toPremiumParams(users.alice, aliceShares_, aliceAssets_, 72, aliceAssets_, 73, block.timestamp + 20 minutes); + params_[1] = _toPremiumParams(users.carol, carolShares_, carolAssets_, 74, carolAssets_, 75, block.timestamp + 21 minutes); + + vm.expectEmit(true, false, false, true, address(migrationHelper)); + emit BatchMigrationToPremiumExecuted(address(users.bob.deleGator), 2); + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToPremiumByDelegationBatch(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(BASE_VAULT.balanceOf(address(users.carol.deleGator)), 0); + assertGt(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(PREMIUM_VAULT.balanceOf(address(users.carol.deleGator)), 0); + _assertNoAdapterDust(); + } + + function test_migrateToBaseBatch_movesExactAmountsForEachDelegator() public { + uint256 aliceAmount_ = 250e6; + uint256 carolAmount_ = 350e6; + uint256 aliceShares_ = _depositToPremium(users.alice, aliceAmount_, 80, block.timestamp + 20 minutes); + uint256 carolShares_ = _depositToPremium(users.carol, carolAmount_, 81, block.timestamp + 21 minutes); + + uint256 aliceAssets_ = _quotePremiumAssets(aliceShares_); + uint256 carolAssets_ = _quotePremiumAssets(carolShares_); + VaultMigrationHelper.ToBaseParams[] memory params_ = new VaultMigrationHelper.ToBaseParams[](2); + params_[0] = _toBaseParams(users.alice, aliceShares_, 82, aliceAssets_, 83); + params_[1] = _toBaseParams(users.carol, carolShares_, 84, carolAssets_, 85); + + vm.expectEmit(true, false, false, true, address(migrationHelper)); + emit BatchMigrationToBaseExecuted(address(users.bob.deleGator), 2); + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToBaseByDelegationBatch(params_); + + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(PREMIUM_VAULT.balanceOf(address(users.carol.deleGator)), 0); + assertGt(BASE_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertGt(BASE_VAULT.balanceOf(address(users.carol.deleGator)), 0); + _assertNoAdapterDust(); + } + + function test_migrateToPremiumBatch_revertsAllWhenLaterStreamFails() public { + uint256 aliceShares_ = _depositToBase(users.alice, 300e6, 90); + uint256 carolShares_ = _depositToBase(users.carol, 400e6, 91); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + + uint256 aliceAssets_ = _quoteBaseAssets(aliceShares_); + uint256 carolAssets_ = _quoteBaseAssets(carolShares_); + VaultMigrationHelper.ToPremiumParams[] memory params_ = new VaultMigrationHelper.ToPremiumParams[](2); + params_[0] = _toPremiumParams(users.alice, aliceShares_, aliceAssets_, 92, aliceAssets_, 93, block.timestamp + 20 minutes); + params_[1] = _toPremiumParams(users.carol, carolShares_, carolAssets_, 94, carolAssets_, 95, block.timestamp + 21 minutes); + params_[1].minimumMint = type(uint256).max; + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegationBatch(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), aliceShares_); + assertEq(BASE_VAULT.balanceOf(address(users.carol.deleGator)), carolShares_); + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + assertEq(PREMIUM_VAULT.balanceOf(address(users.carol.deleGator)), 0); + } + + function test_emptyBatchesRevert() public { + VaultMigrationHelper.ToPremiumParams[] memory premiumParams_ = new VaultMigrationHelper.ToPremiumParams[](0); + VaultMigrationHelper.ToBaseParams[] memory baseParams_ = new VaultMigrationHelper.ToBaseParams[](0); + + vm.expectRevert(VaultMigrationHelper.InvalidBatchLength.selector); + migrationHelper.migrateToPremiumByDelegationBatch(premiumParams_); + + vm.expectRevert(VaultMigrationHelper.InvalidBatchLength.selector); + migrationHelper.migrateToBaseByDelegationBatch(baseParams_); + + VaultMigrationHelper.PremiumTransferParams[] memory transferParams_ = new VaultMigrationHelper.PremiumTransferParams[](0); + vm.expectRevert(VaultMigrationHelper.InvalidBatchLength.selector); + migrationHelper.premiumTransferBatch(transferParams_); + } + + function test_migrateToPremium_revertsOnExpiredCompliance() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 100); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 101, quotedAssets_, 102, block.timestamp - 1); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), baseShares_); + assertEq(PREMIUM_VAULT.balanceOf(address(users.alice.deleGator)), 0); + } + + function test_migrateToPremium_revertsOnWrongComplianceSigner() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 110); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 111, quotedAssets_, 112, block.timestamp + 30 minutes); + params_.compliance = IComplianceVedaTeller.ComplianceData({ + deadline: block.timestamp + 30 minutes, + signature: _signCompliance(users.alice, quotedAssets_, block.timestamp + 30 minutes, 0xBAD) + }); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), baseShares_); + } + + function test_migrateToPremium_revertsOnReplayedCompliance() public { + uint256 firstShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 120); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 firstAssets_ = _quoteBaseAssets(firstShares_); + uint256 deadline_ = block.timestamp + 30 minutes; + VaultMigrationHelper.ToPremiumParams memory firstParams_ = + _toPremiumParams(users.alice, firstShares_, firstAssets_, 121, firstAssets_, 122, deadline_); + + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToPremiumByDelegation(firstParams_); + + uint256 secondShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 123); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 secondAssets_ = _quoteBaseAssets(secondShares_); + VaultMigrationHelper.ToPremiumParams memory replayParams_ = + _toPremiumParams(users.alice, secondShares_, secondAssets_, 124, secondAssets_, 125, deadline_ + 1 hours); + replayParams_.compliance = firstParams_.compliance; + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(replayParams_); + } + + function test_migrateToPremium_revertsOnWithdrawSlippage() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 130); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 131, quotedAssets_, 132, block.timestamp + 30 minutes); + params_.minimumAssets = type(uint256).max; + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(params_); + + assertEq(BASE_VAULT.balanceOf(address(users.alice.deleGator)), baseShares_); + } + + function test_migrateToPremium_revertsOnDelegationReplay() public { + uint256 baseShares_ = _depositToBase(users.alice, DEPOSIT_AMOUNT, 140); + vm.warp(block.timestamp + SHARE_LOCK_SECONDS); + uint256 quotedAssets_ = _quoteBaseAssets(baseShares_); + VaultMigrationHelper.ToPremiumParams memory params_ = + _toPremiumParams(users.alice, baseShares_, quotedAssets_, 141, quotedAssets_, 142, block.timestamp + 30 minutes); + + vm.prank(address(users.bob.deleGator)); + migrationHelper.migrateToPremiumByDelegation(params_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + migrationHelper.migrateToPremiumByDelegation(params_); + } + + function test_premiumTransfer_movesFullBalanceToRecipient() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 200, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 201, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.expectEmit(true, true, false, true, address(migrationHelper)); + emit PremiumTransferExecuted(from_, to_, shares_); + vm.prank(address(users.bob.deleGator)); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), 0); + assertEq(PREMIUM_VAULT.balanceOf(to_), shares_); + _assertNoAdapterDust(); + } + + function test_premiumTransfer_isPermissionless() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 210, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 211, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(address(users.carol.deleGator)); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), 0); + assertEq(PREMIUM_VAULT.balanceOf(to_), shares_); + _assertNoAdapterDust(); + } + + function test_premiumTransfer_revertsOnExpiredCompliance() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 220, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 221, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp - 1, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsOnWrongComplianceSigner() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 230, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 231, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, 0xBAD); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + } + + function test_premiumTransfer_revertsOnReplayedCompliance() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 240, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + uint256 deadline_ = block.timestamp + 30 minutes; + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, deadline_, COMPLIANCE_SIGNER_KEY); + Delegation[] memory firstDelegations_ = _createPremiumTransferChain(users.alice, 241, address(migrationHelper)); + bytes memory firstUserSignature_ = _userTransferSignature(users.alice, to_, firstDelegations_); + + vm.prank(address(users.bob.deleGator)); + _callPremiumTransfer(from_, to_, firstDelegations_, compliance_, firstUserSignature_); + + vm.startPrank(PREMIUM_ROLES_AUTHORITY.owner()); + PREMIUM_ROLES_AUTHORITY.setUserRole(to_, TRANSFER_ALLOWED_ROLE, true); + vm.stopPrank(); + vm.prank(to_); + PREMIUM_VAULT.transfer(from_, shares_); + Delegation[] memory secondDelegations_ = _createPremiumTransferChain(users.alice, 243, address(migrationHelper)); + bytes memory secondUserSignature_ = _userTransferSignature(users.alice, to_, secondDelegations_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, secondDelegations_, compliance_, secondUserSignature_); + } + + function test_premiumTransfer_revertsOnFromMismatch() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 250, block.timestamp + 30 minutes); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 251, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = _transferComplianceData( + address(users.carol.deleGator), to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY + ); + + vm.expectRevert(VaultMigrationHelper.DelegatorMismatch.selector); + _callPremiumTransfer(address(users.carol.deleGator), to_, delegations_, compliance_, hex""); + } + + function test_premiumTransfer_revertsOnComplianceAmountMismatch() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 260, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 261, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_ - 1, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsOnShortDelegationChain() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 270, block.timestamp + 30 minutes); + Delegation[] memory delegations_ = new Delegation[](1); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + + vm.expectRevert(VaultMigrationHelper.InvalidDelegationsLength.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, hex""); + } + + function test_premiumTransfer_revertsOnZeroTo() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 280, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 281, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, address(0), shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + + vm.expectRevert(VaultMigrationHelper.InvalidZeroAddress.selector); + _callPremiumTransfer(from_, address(0), delegations_, compliance_, hex""); + } + + function test_premiumTransfer_revertsWhenHelperNotAllowlisted() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 290, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 291, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(PREMIUM_ROLES_AUTHORITY.owner()); + PREMIUM_ROLES_AUTHORITY.setUserRole(address(migrationHelper), TRANSFER_ALLOWED_ROLE, false); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsWhenCalldataToIsNotHelper() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 300, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 301, to_); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert("AllowedCalldataEnforcer:invalid-calldata"); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsOnDepositComplianceSignature() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 310, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + uint256 deadline_ = block.timestamp + 30 minutes; + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 311, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory depositCompliance_ = IComplianceVedaTeller.ComplianceData({ + deadline: deadline_, signature: _signCompliance(users.alice, shares_, deadline_, COMPLIANCE_SIGNER_KEY) + }); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, delegations_, depositCompliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsWhenComplianceDisabled() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 320, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 321, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.mockCall( + address(PREMIUM_TELLER), + abi.encodeWithSelector(IComplianceVedaTeller.complianceSignerRole.selector), + abi.encode(type(uint8).max) + ); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceDisabled.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + vm.clearMockedCalls(); + } + + function test_premiumTransfer_revertsOnUserSignatureForDifferentTo() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 350, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + address otherTo_ = makeAddr("otherPremiumRecipient"); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 351, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, otherTo_, delegations_); + + vm.expectRevert(VaultMigrationHelper.InvalidUserSignature.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsOnUserSignatureForDifferentRoot() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 360, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + Delegation[] memory signedDelegations_ = _createPremiumTransferChain(users.alice, 361, address(migrationHelper)); + Delegation[] memory usedDelegations_ = _createPremiumTransferChain(users.alice, 362, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, to_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, signedDelegations_); + + vm.expectRevert(VaultMigrationHelper.InvalidUserSignature.selector); + _callPremiumTransfer(from_, to_, usedDelegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransfer_revertsOnComplianceSignatureForDifferentTo() public { + uint256 shares_ = _depositToPremium(users.alice, DEPOSIT_AMOUNT, 370, block.timestamp + 30 minutes); + address from_ = address(users.alice.deleGator); + address to_ = address(users.carol.deleGator); + address otherTo_ = makeAddr("unapprovedPremiumRecipient"); + Delegation[] memory delegations_ = _createPremiumTransferChain(users.alice, 371, address(migrationHelper)); + IComplianceVedaTeller.ComplianceData memory compliance_ = + _transferComplianceData(from_, otherTo_, shares_, block.timestamp + 30 minutes, COMPLIANCE_SIGNER_KEY); + bytes memory userSignature_ = _userTransferSignature(users.alice, to_, delegations_); + + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + _callPremiumTransfer(from_, to_, delegations_, compliance_, userSignature_); + + assertEq(PREMIUM_VAULT.balanceOf(from_), shares_); + } + + function test_premiumTransferBatch_movesEachDelegatorToRecipient() public { + uint256 aliceShares_ = _depositToPremium(users.alice, 300e6, 330, block.timestamp + 20 minutes); + uint256 carolShares_ = _depositToPremium(users.carol, 400e6, 331, block.timestamp + 21 minutes); + address recipient_ = makeAddr("premiumTransferRecipient"); + address alice_ = address(users.alice.deleGator); + address carol_ = address(users.carol.deleGator); + + VaultMigrationHelper.PremiumTransferParams[] memory params_ = new VaultMigrationHelper.PremiumTransferParams[](2); + params_[0] = _premiumTransferParams(users.alice, aliceShares_, 332, recipient_, block.timestamp + 20 minutes); + params_[1] = _premiumTransferParams(users.carol, carolShares_, 333, recipient_, block.timestamp + 21 minutes); + + vm.expectEmit(true, false, false, true, address(migrationHelper)); + emit BatchPremiumTransferExecuted(address(users.bob.deleGator), 2); + vm.prank(address(users.bob.deleGator)); + migrationHelper.premiumTransferBatch(params_); + + assertEq(PREMIUM_VAULT.balanceOf(alice_), 0); + assertEq(PREMIUM_VAULT.balanceOf(carol_), 0); + assertEq(PREMIUM_VAULT.balanceOf(recipient_), aliceShares_ + carolShares_); + _assertNoAdapterDust(); + } + + function test_premiumTransferBatch_revertsAllWhenLaterStreamFails() public { + uint256 aliceShares_ = _depositToPremium(users.alice, 300e6, 340, block.timestamp + 20 minutes); + uint256 carolShares_ = _depositToPremium(users.carol, 400e6, 341, block.timestamp + 21 minutes); + address recipient_ = makeAddr("premiumTransferRecipient"); + address alice_ = address(users.alice.deleGator); + address carol_ = address(users.carol.deleGator); + + VaultMigrationHelper.PremiumTransferParams[] memory params_ = new VaultMigrationHelper.PremiumTransferParams[](2); + params_[0] = _premiumTransferParams(users.alice, aliceShares_, 342, recipient_, block.timestamp + 20 minutes); + params_[1] = _premiumTransferParams(users.carol, carolShares_, 343, recipient_, block.timestamp - 1); + + vm.prank(address(users.bob.deleGator)); + vm.expectRevert(VaultMigrationHelper.ComplianceCheckFailed.selector); + migrationHelper.premiumTransferBatch(params_); + + assertEq(PREMIUM_VAULT.balanceOf(alice_), aliceShares_); + assertEq(PREMIUM_VAULT.balanceOf(carol_), carolShares_); + assertEq(PREMIUM_VAULT.balanceOf(recipient_), 0); + _assertNoAdapterDust(); + } + + function test_withdrawEmergency_revertsOnNonOwner() public { + BasicERC20 testToken_ = new BasicERC20(adapterOwner, "TestToken", "TST", 0); + vm.prank(adapterOwner); + testToken_.mint(address(migrationHelper), 100 ether); + + vm.prank(address(users.alice.deleGator)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(users.alice.deleGator))); + migrationHelper.withdrawEmergency(testToken_, 50 ether, address(users.alice.deleGator)); + + assertEq(testToken_.balanceOf(address(migrationHelper)), 100 ether); + } + + function test_withdrawEmergency_recoverTokens() public { + BasicERC20 testToken_ = new BasicERC20(adapterOwner, "TestToken", "TST", 0); + vm.prank(adapterOwner); + testToken_.mint(address(migrationHelper), 100 ether); + + vm.expectEmit(true, true, false, true, address(migrationHelper)); + emit StuckTokensWithdrawn(testToken_, address(users.alice.deleGator), 50 ether); + + vm.prank(adapterOwner); + migrationHelper.withdrawEmergency(testToken_, 50 ether, address(users.alice.deleGator)); + + assertEq(testToken_.balanceOf(address(migrationHelper)), 50 ether); + assertEq(testToken_.balanceOf(address(users.alice.deleGator)), 50 ether); + } + + function test_withdrawEmergency_revertsOnZeroRecipient() public { + BasicERC20 testToken_ = new BasicERC20(adapterOwner, "TestToken", "TST", 0); + vm.prank(adapterOwner); + testToken_.mint(address(migrationHelper), 100 ether); + + vm.expectRevert(VaultMigrationHelper.InvalidRecipient.selector); + vm.prank(adapterOwner); + migrationHelper.withdrawEmergency(testToken_, 50 ether, address(0)); + } + + function _configureBaseAdapterRole() internal { + vm.prank(BASE_ROLES_AUTHORITY.owner()); + BASE_ROLES_AUTHORITY.setUserRole(address(baseAdapter), BASE_ADAPTER_ROLE, true); + } + + function _configurePremiumForkRoles() internal { + address authorityOwner_ = PREMIUM_ROLES_AUTHORITY.owner(); + address tellerOwner_ = ITellerConfiguration(address(PREMIUM_TELLER)).owner(); + assertEq(authorityOwner_, tellerOwner_); + + vm.startPrank(authorityOwner_); + PREMIUM_ROLES_AUTHORITY.setUserRole(complianceSigner, COMPLIANCE_SIGNER_ROLE, true); + PREMIUM_ROLES_AUTHORITY.setUserRole(address(premiumAdapter), TRANSFER_ALLOWED_ROLE, true); + PREMIUM_ROLES_AUTHORITY.setUserRole(address(migrationHelper), TRANSFER_ALLOWED_ROLE, true); + PREMIUM_ROLES_AUTHORITY.setUserRole(address(premiumAdapter), TEST_ROUTER_ROLE, true); + PREMIUM_ROLES_AUTHORITY.setRoleCapability( + TEST_ROUTER_ROLE, address(PREMIUM_TELLER), IComplianceVedaTeller.withdraw.selector, true + ); + ITellerConfiguration(address(PREMIUM_TELLER)).setTransferRestrictions(TRANSFER_ALLOWED_ROLE, TEST_ROUTER_ROLE); + vm.stopPrank(); + } + + function _fundUser(TestUser memory _user) internal { + vm.prank(MUSD_WHALE); + require(MUSD.transfer(address(_user.deleGator), INITIAL_MUSD_BALANCE), "mUSD funding failed"); + } + + function _quoteBaseAssets(uint256 _shares) internal view returns (uint256) { + return _quoteAssets(BASE_ACCOUNTANT, _shares); + } + + function _quotePremiumAssets(uint256 _shares) internal view returns (uint256) { + return _quoteAssets(PREMIUM_ACCOUNTANT, _shares); + } + + function _quoteAssets(IVedaAccountant _accountant, uint256 _shares) internal view returns (uint256) { + return _shares * _accountant.getRateInQuoteSafe(address(MUSD)) / (10 ** _accountant.decimals()); + } + + function _depositToBase(TestUser memory _delegator, uint256 _amount, uint256 _salt) internal returns (uint256 shares_) { + uint256 sharesBefore_ = BASE_VAULT.balanceOf(address(_delegator.deleGator)); + Delegation[] memory delegations_ = _createDelegationChain(_delegator, address(baseAdapter), address(MUSD), _amount, _salt); + + vm.prank(address(users.bob.deleGator)); + baseAdapter.depositByDelegation(delegations_, 0); + shares_ = BASE_VAULT.balanceOf(address(_delegator.deleGator)) - sharesBefore_; + } + + function _withdrawFromBase( + TestUser memory _delegator, + uint256 _shareAmount, + uint256 _salt + ) + internal + returns (uint256 assetsOut_) + { + uint256 musdBefore_ = MUSD.balanceOf(address(_delegator.deleGator)); + Delegation[] memory delegations_ = + _createDelegationChain(_delegator, address(baseAdapter), address(BASE_VAULT), _shareAmount, _salt); + + vm.prank(address(users.bob.deleGator)); + baseAdapter.withdrawByDelegation(delegations_, 0); + assetsOut_ = MUSD.balanceOf(address(_delegator.deleGator)) - musdBefore_; + } + + function _depositToPremium( + TestUser memory _delegator, + uint256 _amount, + uint256 _salt, + uint256 _deadline + ) + internal + returns (uint256 shares_) + { + uint256 sharesBefore_ = PREMIUM_VAULT.balanceOf(address(_delegator.deleGator)); + Delegation[] memory delegations_ = + _createDelegationChain(_delegator, address(premiumAdapter), address(MUSD), _amount, _salt); + IComplianceVedaTeller.ComplianceData memory compliance_ = _complianceData(_delegator, _amount, _deadline); + + vm.prank(address(users.bob.deleGator)); + premiumAdapter.depositByDelegation(delegations_, 0, compliance_); + shares_ = PREMIUM_VAULT.balanceOf(address(_delegator.deleGator)) - sharesBefore_; + } + + function _withdrawFromPremium(TestUser memory _delegator, uint256 _shareAmount, uint256 _salt) internal { + Delegation[] memory delegations_ = + _createDelegationChain(_delegator, address(premiumAdapter), address(PREMIUM_VAULT), _shareAmount, _salt); + + vm.prank(address(users.bob.deleGator)); + premiumAdapter.withdrawByDelegation(delegations_, 0); + } + + function _toPremiumParams( + TestUser memory _delegator, + uint256 _shareAmount, + uint256 _assetAmount, + uint256 _withdrawalSalt, + uint256 _depositAmount, + uint256 _depositSalt, + uint256 _deadline + ) + internal + view + returns (VaultMigrationHelper.ToPremiumParams memory) + { + return VaultMigrationHelper.ToPremiumParams({ + withdrawalDelegations: _createDelegationChain( + _delegator, address(baseAdapter), address(BASE_VAULT), _shareAmount, _withdrawalSalt + ), + minimumAssets: 0, + depositDelegations: _createDelegationChain( + _delegator, address(premiumAdapter), address(MUSD), _depositAmount, _depositSalt + ), + minimumMint: 0, + compliance: _complianceData(_delegator, _assetAmount, _deadline) + }); + } + + function _toBaseParams( + TestUser memory _delegator, + uint256 _shareAmount, + uint256 _withdrawalSalt, + uint256 _depositAmount, + uint256 _depositSalt + ) + internal + view + returns (VaultMigrationHelper.ToBaseParams memory) + { + return VaultMigrationHelper.ToBaseParams({ + withdrawalDelegations: _createDelegationChain( + _delegator, address(premiumAdapter), address(PREMIUM_VAULT), _shareAmount, _withdrawalSalt + ), + minimumAssets: 0, + depositDelegations: _createDelegationChain( + _delegator, address(baseAdapter), address(MUSD), _depositAmount, _depositSalt + ), + minimumMint: 0 + }); + } + + function _callPremiumTransfer( + address _from, + address _to, + Delegation[] memory _delegations, + IComplianceVedaTeller.ComplianceData memory _compliance, + bytes memory _userSignature + ) + internal + { + migrationHelper.premiumTransfer( + VaultMigrationHelper.PremiumTransferParams({ + from: _from, to: _to, delegations: _delegations, compliance: _compliance, userSignature: _userSignature + }) + ); + } + + function _premiumTransferParams( + TestUser memory _delegator, + uint256 _amount, + uint256 _salt, + address _to, + uint256 _deadline + ) + internal + view + returns (VaultMigrationHelper.PremiumTransferParams memory) + { + address from_ = address(_delegator.deleGator); + Delegation[] memory delegations_ = _createPremiumTransferChain(_delegator, _salt, address(migrationHelper)); + return VaultMigrationHelper.PremiumTransferParams({ + from: from_, + to: _to, + delegations: delegations_, + compliance: _transferComplianceData(from_, _to, _amount, _deadline, COMPLIANCE_SIGNER_KEY), + userSignature: _userTransferSignature(_delegator, _to, delegations_) + }); + } + + function _createDelegationChain( + TestUser memory _delegator, + address _adapter, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation[] memory delegations_) + { + Delegation memory root_ = _createTransferDelegation(_delegator, _adapter, _token, type(uint256).max, _salt); + bytes32 authority_ = EncoderLib._getDelegationHash(root_); + Delegation memory leaf_ = _createAdapterRedelegation(_adapter, authority_, _token, _amount, _salt); + + delegations_ = new Delegation[](2); + delegations_[0] = leaf_; + delegations_[1] = root_; + } + + function _createTransferDelegation( + TestUser memory _delegator, + address _adapter, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](2); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(_token, _amount) }); + caveats_[1] = Caveat({ args: hex"", enforcer: address(redeemerEnforcer), terms: abi.encodePacked(_adapter) }); + + Delegation memory delegation_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(_delegator.deleGator), + authority: ROOT_AUTHORITY, + caveats: caveats_, + salt: _salt, + signature: hex"" + }); + return signDelegation(_delegator, delegation_); + } + + function _createAdapterRedelegation( + address _adapter, + bytes32 _authority, + address _token, + uint256 _amount, + uint256 _salt + ) + internal + view + returns (Delegation memory) + { + Caveat[] memory caveats_ = new Caveat[](1); + caveats_[0] = + Caveat({ args: hex"", enforcer: address(erc20TransferAmountEnforcer), terms: abi.encodePacked(_token, _amount) }); + + Delegation memory delegation_ = Delegation({ + delegate: _adapter, + delegator: address(users.bob.deleGator), + authority: _authority, + caveats: caveats_, + salt: _salt, + signature: hex"" + }); + return signDelegation(users.bob, delegation_); + } + + function _createPremiumTransferChain(TestUser memory _delegator, uint256 _salt, address _calldataTo) + internal + view + returns (Delegation[] memory delegations_) + { + Caveat[] memory rootCaveats_ = new Caveat[](3); + rootCaveats_[0] = + Caveat({ args: hex"", enforcer: address(redeemerEnforcer), terms: abi.encodePacked(address(migrationHelper)) }); + rootCaveats_[1] = Caveat({ + args: hex"", + enforcer: address(allowedCalldataEnforcer), + terms: abi.encodePacked(uint256(0), IERC20.transfer.selector, abi.encode(_calldataTo)) + }); + rootCaveats_[2] = + Caveat({ args: hex"", enforcer: address(allowedTargetsEnforcer), terms: abi.encodePacked(address(PREMIUM_VAULT)) }); + + Delegation memory root_ = Delegation({ + delegate: address(users.bob.deleGator), + delegator: address(_delegator.deleGator), + authority: ROOT_AUTHORITY, + caveats: rootCaveats_, + salt: _salt, + signature: hex"" + }); + root_ = signDelegation(_delegator, root_); + + Caveat[] memory leafCaveats_ = new Caveat[](0); + Delegation memory leaf_ = Delegation({ + delegate: address(migrationHelper), + delegator: address(users.bob.deleGator), + authority: EncoderLib._getDelegationHash(root_), + caveats: leafCaveats_, + salt: _salt, + signature: hex"" + }); + leaf_ = signDelegation(users.bob, leaf_); + + delegations_ = new Delegation[](2); + delegations_[0] = leaf_; + delegations_[1] = root_; + } + + function _userTransferSignature( + TestUser memory _delegator, + address _to, + Delegation[] memory _delegations + ) + internal + view + returns (bytes memory) + { + bytes32 messageHash_ = keccak256( + abi.encode( + address(migrationHelper), + block.chainid, + address(_delegator.deleGator), + _to, + _delegations[_delegations.length - 1].signature + ) + ); + bytes32 ethSignedMessageHash_ = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash_)); + // Destination proof is EIP-191 personal_sign (same prefix as compliance), not EIP-712 like the chain. + return signHash(_delegator, ethSignedMessageHash_); + } + + function _transferComplianceData( + address _from, + address _to, + uint256 _amount, + uint256 _deadline, + uint256 _signerKey + ) + internal + view + returns (IComplianceVedaTeller.ComplianceData memory) + { + bytes32 messageHash_ = keccak256( + abi.encode( + address(migrationHelper), + address(PREMIUM_TELLER), + block.chainid, + _from, + _to, + address(PREMIUM_VAULT), + _amount, + _deadline + ) + ); + bytes32 ethSignedMessageHash_ = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash_)); + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, ethSignedMessageHash_); + return IComplianceVedaTeller.ComplianceData({ deadline: _deadline, signature: abi.encodePacked(r_, s_, v_) }); + } + + function _complianceData( + TestUser memory _delegator, + uint256 _amount, + uint256 _deadline + ) + internal + view + returns (IComplianceVedaTeller.ComplianceData memory) + { + return IComplianceVedaTeller.ComplianceData({ + deadline: _deadline, signature: _signCompliance(_delegator, _amount, _deadline, COMPLIANCE_SIGNER_KEY) + }); + } + + function _signCompliance( + TestUser memory _delegator, + uint256 _amount, + uint256 _deadline, + uint256 _signerKey + ) + internal + view + returns (bytes memory) + { + bytes32 messageHash_ = keccak256( + abi.encode( + address(PREMIUM_TELLER), + block.chainid, + address(premiumAdapter), + address(_delegator.deleGator), + address(MUSD), + _amount, + _deadline + ) + ); + bytes32 ethSignedMessageHash_ = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash_)); + (uint8 v_, bytes32 r_, bytes32 s_) = vm.sign(_signerKey, ethSignedMessageHash_); + return abi.encodePacked(r_, s_, v_); + } + + function _assertNoAdapterDust() internal { + assertEq(MUSD.balanceOf(address(baseAdapter)), 0); + assertEq(MUSD.balanceOf(address(premiumAdapter)), 0); + assertEq(MUSD.balanceOf(address(migrationHelper)), 0); + assertEq(BASE_VAULT.balanceOf(address(baseAdapter)), 0); + assertEq(PREMIUM_VAULT.balanceOf(address(premiumAdapter)), 0); + assertEq(BASE_VAULT.balanceOf(address(migrationHelper)), 0); + assertEq(PREMIUM_VAULT.balanceOf(address(migrationHelper)), 0); + } +}