diff --git a/auction-server/src/auction.rs b/auction-server/src/auction.rs index 63d8e24e..3f35324c 100644 --- a/auction-server/src/auction.rs +++ b/auction-server/src/auction.rs @@ -49,6 +49,7 @@ use { Bytes, TransactionReceipt, TransactionRequest, + H160, U256, }, }, @@ -90,26 +91,29 @@ impl TryFrom for Provider { } } +impl From<(H160, Bytes, U256)> for MulticallData { + fn from(x: (H160, Bytes, U256)) -> Self { + MulticallData { + target_contract: x.0, + target_calldata: x.1, + bid_amount: x.2, + } + } +} + pub fn get_simulation_call( relayer: Address, provider: Provider, chain_config: EthereumConfig, permission_key: Bytes, - target_contracts: Vec
, - target_calldata: Vec, - bid_amounts: Vec, + multicall_data: Vec, ) -> FunctionCall>, Provider, Vec> { let client = Arc::new(provider); let express_relay_contract = ExpressRelayContract::new(chain_config.express_relay_contract, client); express_relay_contract - .multicall( - permission_key, - target_contracts, - target_calldata, - bid_amounts, - ) + .multicall(permission_key, multicall_data) .from(relayer) } @@ -135,19 +139,9 @@ pub async fn simulate_bids( provider: Provider, chain_config: EthereumConfig, permission: Bytes, - target_contracts: Vec
, - target_calldata: Vec, - bid_amounts: Vec, + multicall_data: Vec, ) -> Result<(), SimulationError> { - let call = get_simulation_call( - relayer, - provider, - chain_config, - permission, - target_contracts, - target_calldata, - bid_amounts, - ); + let call = get_simulation_call(relayer, provider, chain_config, permission, multicall_data); match call.await { Ok(results) => { evaluate_simulation_results(results)?; @@ -189,9 +183,7 @@ pub async fn submit_bids( chain_config: EthereumConfig, network_id: u64, permission: Bytes, - target_contracts: Vec
, - target_calldata: Vec, - bid_amounts: Vec, + multicall_data: Vec, ) -> Result, SubmissionError> { let transformer = LegacyTxTransformer { use_legacy_tx: chain_config.legacy_tx, @@ -203,12 +195,8 @@ pub async fn submit_bids( let express_relay_contract = SignableExpressRelayContract::new(chain_config.express_relay_contract, client); - let call = express_relay_contract.multicall( - permission, - target_contracts, - target_calldata, - bid_amounts, - ); + + let call = express_relay_contract.multicall(permission, multicall_data); let mut gas_estimate = call .estimate_gas() .await @@ -255,9 +243,7 @@ pub async fn run_submission_loop(store: Arc) -> Result<()> { chain_store.config.clone(), chain_store.network_id, permission_key.clone(), - winner_bids.iter().map(|b| b.target_contract).collect(), - winner_bids.iter().map(|b| b.target_calldata.clone()).collect(), - winner_bids.iter().map(|b| b.bid_amount).collect(), + winner_bids.iter().map(|b| MulticallData::from((b.target_contract, b.target_calldata.clone(), b.bid_amount))).collect() ) .await; match submission { @@ -323,9 +309,11 @@ pub async fn handle_bid(store: Arc, bid: Bid) -> result::Result uint256) _feeConfig; - mapping(bytes32 => bool) _permissions; - uint256 _defaultFee; - /** - * @notice ExpressRelay constructor - Initializes a new multicall contract with given parameters + * @notice ExpressRelay constructor - Initializes a new ExpressRelay contract with given parameters * - * @param operator: address of express relay operator EOA - * @param defaultFee: default fee split to be paid to the protocol whose permissioning is being used + * @param admin: address of admin of express relay + * @param relayer: address of relayer EOA + * @param feeSplitProtocolDefault: default fee split to be paid to the protocol whose permissioning is being used + * @param feeSplitRelayer: split of the non-protocol fees to be paid to the relayer */ - constructor(address operator, uint256 defaultFee) { - _operator = operator; - _defaultFee = defaultFee; - } - - /** - * @notice getOperator function - returns the address of the express relay operator - */ - function getOperator() public view returns (address) { - return _operator; - } - - function isPermissioned( - address protocolFeeReceiver, - bytes calldata permissionId - ) public view returns (bool permissioned) { - return - _permissions[ - keccak256(abi.encode(protocolFeeReceiver, permissionId)) - ]; - } - - /** - * @notice setFee function - sets the fee for a given fee recipient - * - * @param feeRecipient: address of the fee recipient for the contract being registered - * @param feeSplit: amount of fee to be split with the protocol. 10**18 is 100% - */ - function setFee(address feeRecipient, uint256 feeSplit) public { - if (msg.sender != _operator) { - revert Unauthorized(); - } - _feeConfig[feeRecipient] = feeSplit; - } - - function _isContract(address _addr) private view returns (bool) { - uint32 size; - assembly { - size := extcodesize(_addr) - } - return (size > 0); - } - - function _bytesToAddress( - bytes memory bys - ) private pure returns (address addr) { - // this does not assume the struct fields of the permission key - addr = address(uint160(uint256(bytes32(bys)))); + constructor( + address admin, + address relayer, + uint256 feeSplitProtocolDefault, + uint256 feeSplitRelayer + ) { + state.admin = admin; + state.relayer = relayer; + + validateFeeSplit(feeSplitProtocolDefault); + state.feeSplitProtocolDefault = feeSplitProtocolDefault; + + validateFeeSplit(feeSplitRelayer); + state.feeSplitRelayer = feeSplitRelayer; } /** * @notice multicall function - performs a number of calls to external contracts in order * * @param permissionKey: permission to allow for this call - * @param targetContracts: ordered list of contracts to call into - * @param targetCalldata: ordered list of calldata to call the targets with - * @param bidAmounts: ordered list of bids; call i will fail if it does not send this contract at least bid i + * @param multicallData: ordered list of data for multicall, consisting of targetContract, targetCalldata, and bidAmount */ function multicall( bytes calldata permissionKey, - address[] calldata targetContracts, - bytes[] calldata targetCalldata, - uint256[] calldata bidAmounts - ) public payable returns (MulticallStatus[] memory multicallStatuses) { - if (msg.sender != _operator) { - revert Unauthorized(); - } + MulticallData[] calldata multicallData + ) + public + payable + onlyRelayer + returns (MulticallStatus[] memory multicallStatuses) + { if (permissionKey.length < 20) { revert InvalidPermission(); } - _permissions[keccak256(permissionKey)] = true; - multicallStatuses = new MulticallStatus[](targetCalldata.length); + state.permissions[keccak256(permissionKey)] = true; + multicallStatuses = new MulticallStatus[](multicallData.length); uint256 totalBid = 0; - for (uint256 i = 0; i < targetCalldata.length; i++) { - // try/catch will revert if call to searcher fails or if bid conditions not met + for (uint256 i = 0; i < multicallData.length; i++) { try - this.callWithBid( - targetContracts[i], - targetCalldata[i], - bidAmounts[i] - ) + // callWithBid will revert if call to external contract fails or if bid conditions not met + this.callWithBid(multicallData[i]) returns (bool success, bytes memory result) { multicallStatuses[i].externalSuccess = success; multicallStatuses[i].externalResult = result; @@ -117,22 +72,21 @@ contract ExpressRelay is IExpressRelay { // only count bid if call was successful (and bid was paid out) if (multicallStatuses[i].externalSuccess) { - totalBid += bidAmounts[i]; + totalBid += multicallData[i].bidAmount; } } // use the first 20 bytes of permission as fee receiver - address feeReceiver = _bytesToAddress(permissionKey); + address feeReceiver = bytesToAddress(permissionKey); // transfer fee to the protocol - uint256 protocolFee = _feeConfig[feeReceiver]; - if (protocolFee == 0) { - protocolFee = _defaultFee; + uint256 feeSplitProtocol = state.feeConfig[feeReceiver]; + if (feeSplitProtocol == 0) { + feeSplitProtocol = state.feeSplitProtocolDefault; } - uint256 feeProtocolNumerator = totalBid * protocolFee; - if (feeProtocolNumerator > 0) { - uint256 feeProtocol = feeProtocolNumerator / - 1000_000_000_000_000_000; - if (_isContract(feeReceiver)) { + uint256 feeProtocol = (totalBid * feeSplitProtocol) / + state.feeSplitPrecision; + if (feeProtocol > 0) { + if (isContract(feeReceiver)) { IExpressRelayFeeReceiver(feeReceiver).receiveAuctionProceedings{ value: feeProtocol }(permissionKey); @@ -140,25 +94,33 @@ contract ExpressRelay is IExpressRelay { payable(feeReceiver).transfer(feeProtocol); } } - _permissions[keccak256(permissionKey)] = false; + state.permissions[keccak256(permissionKey)] = false; + + // pay the relayer + uint256 feeRelayer = ((totalBid - feeProtocol) * + state.feeSplitRelayer) / state.feeSplitPrecision; + if (feeRelayer > 0) { + payable(state.relayer).transfer(feeRelayer); + } } /** * @notice callWithBid function - contained call to function with check for bid invariant * - * @param targetContract: contract address to call into - * @param targetCalldata: calldata to call the target with - * @param bid: bid to be paid; call will fail if it does not send this contract at least bid, + * @param multicallData: data for multicall, consisting of targetContract, targetCalldata, and bidAmount */ function callWithBid( - address targetContract, - bytes calldata targetCalldata, - uint256 bid + MulticallData calldata multicallData ) public payable returns (bool, bytes memory) { + // manual check for internal call (function is public for try/catch) + if (msg.sender != address(this)) { + revert Unauthorized(); + } + uint256 balanceInitEth = address(this).balance; - (bool success, bytes memory result) = targetContract.call( - targetCalldata + (bool success, bytes memory result) = multicallData.targetContract.call( + multicallData.targetCalldata ); if (success) { @@ -166,7 +128,7 @@ contract ExpressRelay is IExpressRelay { // ensure that this contract was paid at least bid ETH require( - (balanceFinalEth - balanceInitEth >= bid) && + (balanceFinalEth - balanceInitEth >= multicallData.bidAmount) && (balanceFinalEth >= balanceInitEth), "invalid bid" ); diff --git a/per_multicall/src/ExpressRelayHelpers.sol b/per_multicall/src/ExpressRelayHelpers.sol new file mode 100644 index 00000000..689e6dcf --- /dev/null +++ b/per_multicall/src/ExpressRelayHelpers.sol @@ -0,0 +1,19 @@ +// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved +pragma solidity ^0.8.13; + +contract ExpressRelayHelpers { + function isContract(address addr) internal view returns (bool) { + uint32 size; + assembly { + size := extcodesize(addr) + } + return (size > 0); + } + + function bytesToAddress( + bytes memory bys + ) internal pure returns (address addr) { + // extract the first 20 bytes and convert to an address + addr = address(uint160(uint256(bytes32(bys)))); + } +} diff --git a/per_multicall/src/ExpressRelayState.sol b/per_multicall/src/ExpressRelayState.sol new file mode 100644 index 00000000..39df3dcb --- /dev/null +++ b/per_multicall/src/ExpressRelayState.sol @@ -0,0 +1,163 @@ +// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved +pragma solidity ^0.8.13; + +import "./Errors.sol"; +import "./Structs.sol"; + +import "@pythnetwork/express-relay-sdk-solidity/IExpressRelay.sol"; + +contract ExpressRelayStorage { + struct State { + // address of admin of express relay, handles setting fees and relayer role + address admin; + // address of relayer EOA uniquely permissioned to call ExpressRelay.multicall + address relayer; + // stores custom fee splits for protocol fee receivers + mapping(address => uint256) feeConfig; + // stores the flags for whether permission keys are currently allowed + mapping(bytes32 => bool) permissions; + // default fee split for protocol, used if custom fee split is not set + uint256 feeSplitProtocolDefault; + // split of the non-protocol fees to be paid to the relayer + uint256 feeSplitRelayer; + // precision for fee splits + uint256 feeSplitPrecision; + } +} + +contract ExpressRelayState is IExpressRelay { + ExpressRelayStorage.State state; + + constructor() { + state.feeSplitPrecision = 10 ** 18; + } + + modifier onlyAdmin() { + if (msg.sender != state.admin) { + revert Unauthorized(); + } + _; + } + + modifier onlyRelayer() { + if (msg.sender != state.relayer) { + revert Unauthorized(); + } + _; + } + + function validateFeeSplit(uint256 feeSplit) internal view { + if (feeSplit > state.feeSplitPrecision) { + revert InvalidFeeSplit(); + } + } + + /** + * @notice getAdmin function - returns the address of the admin + */ + function getAdmin() public view returns (address) { + return state.admin; + } + + /** + * @notice setRelayer function - sets the relayer + * + * @param relayer: address of the relayer to be set + */ + function setRelayer(address relayer) public onlyAdmin { + state.relayer = relayer; + } + + /** + * @notice getRelayer function - returns the address of the relayer + */ + function getRelayer() public view returns (address) { + return state.relayer; + } + + /** + * @notice setFeeProtocolDefault function - sets the default fee split for the protocol + * + * @param feeSplit: split of fee to be sent to the protocol. 10**18 is 100% + */ + function setFeeProtocolDefault(uint256 feeSplit) public onlyAdmin { + validateFeeSplit(feeSplit); + state.feeSplitProtocolDefault = feeSplit; + } + + /** + * @notice getFeeProtocolDefault function - returns the default fee split for the protocol + */ + function getFeeProtocolDefault() public view returns (uint256) { + return state.feeSplitProtocolDefault; + } + + /** + * @notice setFeeProtocol function - sets the fee split for a given protocol fee recipient + * + * @param feeRecipient: address of the fee recipient for the protocol + * @param feeSplit: split of fee to be sent to the protocol. 10**18 is 100% + */ + function setFeeProtocol( + address feeRecipient, + uint256 feeSplit + ) public onlyAdmin { + validateFeeSplit(feeSplit); + state.feeConfig[feeRecipient] = feeSplit; + } + + /** + * @notice getFeeProtocol function - returns the fee split for a given protocol fee recipient + * + * @param feeRecipient: address of the fee recipient for the protocol + */ + function getFeeProtocol( + address feeRecipient + ) public view returns (uint256) { + uint256 feeProtocol = state.feeConfig[feeRecipient]; + if (feeProtocol == 0) { + feeProtocol = state.feeSplitProtocolDefault; + } + return feeProtocol; + } + + /** + * @notice setFeeRelayer function - sets the fee split for the relayer + * + * @param feeSplit: split of remaining fee (after subtracting protocol fee) to be sent to the relayer. 10**18 is 100% + */ + function setFeeRelayer(uint256 feeSplit) public onlyAdmin { + validateFeeSplit(feeSplit); + state.feeSplitRelayer = feeSplit; + } + + /** + * @notice getFeeRelayer function - returns the fee split for the relayer + */ + function getFeeRelayer() public view returns (uint256) { + return state.feeSplitRelayer; + } + + /** + * @notice getFeeSplitPrecision function - returns the precision for fee splits + */ + function getFeeSplitPrecision() public view returns (uint256) { + return state.feeSplitPrecision; + } + + /** + * @notice isPermissioned function - checks if a given permission key is currently allowed + * + * @param protocolFeeReceiver: address of the protocol fee receiver, first part of permission key + * @param permissionId: arbitrary bytes representing the action being gated, second part of the permission key + */ + function isPermissioned( + address protocolFeeReceiver, + bytes calldata permissionId + ) public view returns (bool permissioned) { + return + state.permissions[ + keccak256(abi.encode(protocolFeeReceiver, permissionId)) + ]; + } +} diff --git a/per_multicall/src/OpportunityAdapter.sol b/per_multicall/src/OpportunityAdapter.sol index a407a52f..5363ecfd 100644 --- a/per_multicall/src/OpportunityAdapter.sol +++ b/per_multicall/src/OpportunityAdapter.sol @@ -18,6 +18,7 @@ abstract contract OpportunityAdapter is SigVerify { /** * @notice OpportunityAdapter constructor - Initializes a new opportunity adapter contract with given parameters * + * @param admin: address of admin of opportunity adapter * @param expressRelay: address of express relay * @param weth: address of WETH contract */ @@ -165,5 +166,5 @@ abstract contract OpportunityAdapter is SigVerify { _signatureUsed[params.signature] = true; } - receive() external payable {} // TODO: can we get rid of this? seems not but unsure why + receive() external payable {} } diff --git a/per_multicall/src/SigVerify.sol b/per_multicall/src/SigVerify.sol index b0334eb2..433c6c70 100644 --- a/per_multicall/src/SigVerify.sol +++ b/per_multicall/src/SigVerify.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.13; import "./Errors.sol"; -import "forge-std/console.sol"; contract SigVerify { function verifyCalldata( diff --git a/per_multicall/src/Structs.sol b/per_multicall/src/Structs.sol index 5c417dc0..4f2d8ffa 100644 --- a/per_multicall/src/Structs.sol +++ b/per_multicall/src/Structs.sol @@ -22,6 +22,12 @@ struct TokenAmount { uint256 amount; } +struct MulticallData { + address targetContract; + bytes targetCalldata; + uint256 bidAmount; +} + struct MulticallStatus { bool externalSuccess; bytes externalResult; diff --git a/per_multicall/test/ExpressRelayIntegration.sol b/per_multicall/test/ExpressRelayIntegration.sol deleted file mode 100644 index a8e315f5..00000000 --- a/per_multicall/test/ExpressRelayIntegration.sol +++ /dev/null @@ -1,1185 +0,0 @@ -// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved -pragma solidity ^0.8.13; - -import {Test, console2} from "forge-std/Test.sol"; -import "../src/SigVerify.sol"; -import "forge-std/console.sol"; -import "forge-std/StdMath.sol"; - -import {TokenVault} from "../src/TokenVault.sol"; -import {SearcherVault} from "../src/SearcherVault.sol"; -import {ExpressRelay} from "../src/ExpressRelay.sol"; -import {WETH9} from "../src/WETH9.sol"; -import {OpportunityAdapter} from "../src/OpportunityAdapter.sol"; -import {MyToken} from "../src/MyToken.sol"; -import "../src/Errors.sol"; -import "../src/TokenVaultErrors.sol"; -import "../src/Structs.sol"; - -import "@pythnetwork/pyth-sdk-solidity/MockPyth.sol"; - -import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; - -import "openzeppelin-contracts/contracts/utils/Strings.sol"; - -import "./helpers/Signatures.sol"; -import "./helpers/PriceHelpers.sol"; -import "./helpers/TestParsingHelpers.sol"; -import "./helpers/MulticallHelpers.sol"; -import "../src/OpportunityAdapterUpgradable.sol"; - -/** - * @title ExpressRelayIntegrationTest - * - * ExpressRelayIntegrationTest is a contract that tests the integration of the various contracts in the ExpressRelay stack. - * This includes the ExpressRelay entrypoint contract for all relay interactions, the TokenVault dummy lending protocol contract, individual searcher contracts programmed to perform liquidations, the OpportunityAdapter contract used to facilitate liquidations directly from searcher EOAs, and the relevant token contracts. - * We test the integration of these contracts by creating vaults in the TokenVault protocol, simulating undercollateralization of these vaults to trigger liquidations, constructing the necessary liquidation data, and then calling liquidation through OpportunityAdapter or the searcher contracts. - * - * The focus in these tests is ensuring that liquidation succeeds (or fails as expected) through the ExpressRelay contrct routing to the searcher contracts or the OpportunityAdapter contract. - */ -contract ExpressRelayIntegrationTest is - Test, - TestParsingHelpers, - Signatures, - PriceHelpers, - MulticallHelpers -{ - TokenVault public tokenVault; - SearcherVault public searcherA; - SearcherVault public searcherB; - ExpressRelay public expressRelay; - WETH9 public weth; - OpportunityAdapterUpgradable public opportunityAdapter; - MockPyth public mockPyth; - - MyToken public token1; - MyToken public token2; - - bytes32 idToken1; - bytes32 idToken2; - - int32 constant tokenExpo = 0; - - address perOperatorAddress; - uint256 perOperatorSk; - address searcherAOwnerAddress; - uint256 searcherAOwnerSk; - address searcherBOwnerAddress; - uint256 searcherBOwnerSk; - address tokenVaultDeployer; - uint256 tokenVaultDeployerSk; - - uint256 constant healthPrecision = 10 ** 16; - - address depositor; // address of the initial depositor into the token vault - - uint256 constant amountToken1DepositorInit = 1_000_000; // amount of token 1 initially owned by the vault depositor - uint256 constant amountToken2DepositorInit = 1_000_000; // amount of token 2 initially owned by the vault depositor - uint256 constant amountToken1AInit = 2_000_000; // amount of token 1 initially owned by searcher A contract - uint256 constant amountToken2AInit = 2_000_000; // amount of token 2 initially owned by searcher A contract - uint256 constant amountToken1BInit = 3_000_000; // amount of token 1 initially owned by searcher B contract - uint256 constant amountToken2BInit = 3_000_000; // amount of token 2 initially owned by searcher B contract - uint256 constant amountToken2TokenVaultInit = 500_000; // amount of token 2 initially owned by the token vault contract (necessary to allow depositor to borrow token 2) - - address[] tokensCollateral; // addresses of collateral, index corresponds to vault number - address[] tokensDebt; // addresses of debt, index corresponds to vault number - uint256[] amountsCollateral; // amounts of collateral, index corresponds to vault number - uint256[] amountsDebt; // amounts of debt, index corresponds to vault number - bytes32[] idsCollateral; // pyth price feed ids of collateral, index corresponds to vault number - bytes32[] idsDebt; // pyth price feed ids of debt, index corresponds to vault number - - // initial token oracle info - int64 constant token1PriceInitial = 100; - uint64 constant token1ConfInitial = 1; - int64 constant token2PriceInitial = 100; - uint64 constant token2ConfInitial = 1; - uint64 constant publishTimeInitial = 1_000_000; - uint64 constant prevPublishTimeInitial = 0; - - int64[] tokenDebtPricesLiqExpressRelay; - int64[] tokenDebtPricesLiqPermissionless; - - uint256 constant defaultFeeSplitProtocol = 50 * 10 ** 16; - - uint256 feeSplitTokenVault; - uint256 constant feeSplitPrecisionTokenVault = 10 ** 18; - - /** - * @notice setUp function - sets up the contracts, wallets, tokens, oracle feeds, and vaults for the test - * - * This function creates the entire environment for the start of each test. It is called before each test. - * This function creates the ExpressRelay, WETH9, OpportunityAdapter, MockPyth, TokenVault, SearcherVault, and two ERC-20 token contracts. The two ERC-20 tokens are used as collateral and debt tokens for the vaults that will be created. - * It also sets up the initial token amounts for the depositor, searcher A, searcher B, and the token vault. Additionally, it sets the initial oracle prices for the tokens. - * The function then sets up two vaults in the TokenVault contract. Each vault's collateral and debt tokens are set, as well as the amounts of each token in the vault. Based on the amounts in the vault and the initial token prices, we back out the liquidation threshold prices--these are used later in the tests to set prices that trigger liquidation. - * Finally, the function funds the searcher wallets with Eth and tokens. It also creates the allowances from the searchers' wallets to the liquidation adapter to use the searcher wallets' tokens and weth to liquidate vaults. - */ - function setUp() public { - setUpWallets(); - setUpContracts(); - setUpTokensAndOracle(); - setUpVaults(); - fundSearcherWallets(); - } - - /** - * @notice setUpWallets function - sets up the wallets for the test - * - * Sets up express relay operator, searcher, initial token vault deployer, and initial vault depositor wallets - */ - function setUpWallets() public { - (perOperatorAddress, perOperatorSk) = makeAddrAndKey("perOperator"); - - (searcherAOwnerAddress, searcherAOwnerSk) = makeAddrAndKey("searcherA"); - (searcherBOwnerAddress, searcherBOwnerSk) = makeAddrAndKey("searcherB"); - - (tokenVaultDeployer, tokenVaultDeployerSk) = makeAddrAndKey( - "tokenVaultDeployer" - ); - - (depositor, ) = makeAddrAndKey("depositor"); - } - - /** - * @notice setUpContracts function - sets up the contracts for the test - * - * Sets up the ExpressRelay, WETH9, OpportunityAdapter, MockPyth, TokenVault, SearcherVault, and ERC-20 token contracts - */ - function setUpContracts() public { - // instantiate multicall contract with ExpressRelay operator as the deployer - vm.prank(perOperatorAddress, perOperatorAddress); - expressRelay = new ExpressRelay( - perOperatorAddress, - defaultFeeSplitProtocol - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - weth = new WETH9(); - - vm.prank(perOperatorAddress, perOperatorAddress); - OpportunityAdapterUpgradable _opportunityAdapter = new OpportunityAdapterUpgradable(); - // deploy proxy contract and point it to implementation - ERC1967Proxy proxy = new ERC1967Proxy(address(_opportunityAdapter), ""); - opportunityAdapter = OpportunityAdapterUpgradable(payable(proxy)); - opportunityAdapter.initialize( - perOperatorAddress, - perOperatorAddress, - address(expressRelay), - address(weth) - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - mockPyth = new MockPyth(1_000_000, 0); - - bool allowUndercollateralized = false; - vm.prank(tokenVaultDeployer, tokenVaultDeployer); // we prank here to standardize the value of the token contract address across different runs - tokenVault = new TokenVault( - address(expressRelay), - address(mockPyth), - allowUndercollateralized - ); - console.log("contract of token vault is", address(tokenVault)); - feeSplitTokenVault = defaultFeeSplitProtocol; - - // instantiate searcher A's contract with searcher A's wallet as the deployer - vm.prank(searcherAOwnerAddress, searcherAOwnerAddress); - searcherA = new SearcherVault( - address(expressRelay), - address(tokenVault) - ); - console.log("contract of searcher A is", address(searcherA)); - - // instantiate searcher B's contract with searcher B's wallet as the deployer - vm.prank(searcherBOwnerAddress, searcherBOwnerAddress); - searcherB = new SearcherVault( - address(expressRelay), - address(tokenVault) - ); - console.log("contract of searcher B is", address(searcherB)); - - vm.prank(perOperatorAddress, perOperatorAddress); - token1 = new MyToken("token1", "T1"); - vm.prank(perOperatorAddress, perOperatorAddress); - token2 = new MyToken("token2", "T2"); - console.log("contract of token1 is", address(token1)); - console.log("contract of token2 is", address(token2)); - } - - /** - * @notice setUpTokensAndOracle function - sets up the tokens for the test and their initial oracle feeds - * - * Sets up the initial token amounts for the depositor, searcher A, searcher B, and the token vault - * Also sets the initial oracle prices for the tokens - */ - function setUpTokensAndOracle() public { - // mint tokens to the depositor address - token1.mint(depositor, amountToken1DepositorInit); - token2.mint(depositor, amountToken2DepositorInit); - - // mint tokens to searcher A contract - token1.mint(address(searcherA), amountToken1AInit); - token2.mint(address(searcherA), amountToken2AInit); - - // mint tokens to searcher B contract - token1.mint(address(searcherB), amountToken1BInit); - token2.mint(address(searcherB), amountToken2BInit); - - // mint token 2 to the vault contract (to allow creation of initial vault with outstanding debt position) - token2.mint(address(tokenVault), amountToken2TokenVaultInit); - - // create token price feed IDs - idToken1 = bytes32(uint256(uint160(address(token1)))); - idToken2 = bytes32(uint256(uint160(address(token2)))); - - vm.warp(publishTimeInitial); - bytes[] memory updateData = new bytes[](2); - updateData[0] = mockPyth.createPriceFeedUpdateData( - idToken1, - token1PriceInitial, - token1ConfInitial, - tokenExpo, - token1PriceInitial, - token1ConfInitial, - publishTimeInitial, - prevPublishTimeInitial - ); - updateData[1] = mockPyth.createPriceFeedUpdateData( - idToken2, - token2PriceInitial, - token2ConfInitial, - tokenExpo, - token2PriceInitial, - token2ConfInitial, - publishTimeInitial, - prevPublishTimeInitial - ); - - mockPyth.updatePriceFeeds(updateData); - } - - /** - * @notice setUpVaults function - sets up the vaults for the test and stores relevant info per vault - */ - function setUpVaults() public { - // set which tokens are collateral and which are debt for each vault - tokensCollateral = new address[](2); - idsCollateral = new bytes32[](2); - tokensCollateral[0] = address(token1); - idsCollateral[0] = idToken1; - tokensCollateral[1] = address(token1); - idsCollateral[1] = idToken1; - - tokensDebt = new address[](2); - idsDebt = new bytes32[](2); - tokensDebt[0] = address(token2); - idsDebt[0] = idToken2; - tokensDebt[1] = address(token2); - idsDebt[1] = idToken2; - - amountsCollateral = new uint256[](2); - amountsCollateral[0] = 100; - amountsCollateral[1] = 200; - - amountsDebt = new uint256[](2); - amountsDebt[0] = 80; - amountsDebt[1] = 150; - - // create vault 0 - uint256 minCollatPERVault0 = 110 * healthPrecision; - uint256 minCollatPermissionlessVault0 = 100 * healthPrecision; - vm.prank(depositor, depositor); - MyToken(tokensCollateral[0]).approve( - address(tokenVault), - amountsCollateral[0] - ); - vm.prank(depositor, depositor); - tokenVault.createVault( - tokensCollateral[0], - tokensDebt[0], - amountsCollateral[0], - amountsDebt[0], - minCollatPERVault0, - minCollatPermissionlessVault0, - idsCollateral[0], - idsDebt[0], - new bytes[](0) - ); - - // create vault 1 - uint256 minCollatPERVault1 = 110 * healthPrecision; - uint256 minCollatPermissionlessVault1 = 100 * healthPrecision; - vm.prank(depositor, depositor); - MyToken(tokensCollateral[1]).approve( - address(tokenVault), - amountsCollateral[1] - ); - vm.prank(depositor, depositor); - tokenVault.createVault( - tokensCollateral[1], - tokensDebt[1], - amountsCollateral[1], - amountsDebt[1], - minCollatPERVault1, - minCollatPermissionlessVault1, - idsCollateral[1], - idsDebt[1], - new bytes[](0) - ); - - int64 priceCollateralVault0; - int64 priceCollateralVault1; - - if (tokensCollateral[0] == address(token1)) { - priceCollateralVault0 = token1PriceInitial; - } else { - priceCollateralVault0 = token2PriceInitial; - } - - int64 tokenDebtPriceLiqPermissionlessVault0; - int64 tokenDebtPriceLiqPERVault0; - int64 tokenDebtPriceLiqPermissionlessVault1; - int64 tokenDebtPriceLiqPERVault1; - - tokenDebtPriceLiqPermissionlessVault0 = getDebtLiquidationPrice( - amountsCollateral[0], - amountsDebt[0], - minCollatPermissionlessVault0, - healthPrecision, - priceCollateralVault0 - ); - - tokenDebtPriceLiqPERVault0 = getDebtLiquidationPrice( - amountsCollateral[0], - amountsDebt[0], - minCollatPERVault0, - healthPrecision, - priceCollateralVault0 - ); - - if (tokensCollateral[1] == address(token1)) { - priceCollateralVault1 = token1PriceInitial; - } else { - priceCollateralVault1 = token2PriceInitial; - } - - tokenDebtPriceLiqPermissionlessVault1 = getDebtLiquidationPrice( - amountsCollateral[1], - amountsDebt[1], - minCollatPermissionlessVault1, - healthPrecision, - priceCollateralVault1 - ); - - tokenDebtPriceLiqPERVault1 = getDebtLiquidationPrice( - amountsCollateral[1], - amountsDebt[1], - minCollatPERVault1, - healthPrecision, - priceCollateralVault1 - ); - - tokenDebtPricesLiqExpressRelay = new int64[](2); - tokenDebtPricesLiqExpressRelay[0] = tokenDebtPriceLiqPERVault0; - tokenDebtPricesLiqExpressRelay[1] = tokenDebtPriceLiqPERVault1; - - tokenDebtPricesLiqPermissionless = new int64[](2); - tokenDebtPricesLiqPermissionless[ - 0 - ] = tokenDebtPriceLiqPermissionlessVault0; - tokenDebtPricesLiqPermissionless[ - 1 - ] = tokenDebtPriceLiqPermissionlessVault1; - } - - /** - * @notice fundSearcherWallets function - funds the searcher wallets with Eth, tokens, and allowances - * - * Funding enables searchers' wallets to directly liquidate via the liquidation adapter - */ - function fundSearcherWallets() public { - // fund searcher A and searcher B - vm.deal(address(searcherA), 1 ether); - vm.deal(address(searcherB), 1 ether); - - address[] memory searchers = new address[](2); - searchers[0] = address(searcherAOwnerAddress); - searchers[1] = address(searcherBOwnerAddress); - - for (uint256 i = 0; i < searchers.length; i++) { - address searcher = searchers[i]; - - // mint tokens to searcher wallet so it can liquidate vaults - MyToken(tokensDebt[0]).mint(address(searcher), amountsDebt[0]); - MyToken(tokensDebt[1]).mint(address(searcher), amountsDebt[1]); - - vm.startPrank(searcher, searcher); - - // create allowance for opportunity adapter - if (tokensDebt[0] == tokensDebt[1]) { - MyToken(tokensDebt[0]).approve( - address(opportunityAdapter), - amountsDebt[0] + amountsDebt[1] - ); - } else { - MyToken(tokensDebt[0]).approve( - address(opportunityAdapter), - amountsDebt[0] - ); - MyToken(tokensDebt[1]).approve( - address(opportunityAdapter), - amountsDebt[1] - ); - } - - // deposit eth into the weth contract - vm.deal(searcher, (i + 1) * 100 ether); - weth.deposit{value: (i + 1) * 100 ether}(); - - // create allowance for opportunity adapter (weth) - weth.approve(address(opportunityAdapter), (i + 1) * 100 ether); - - vm.stopPrank(); - } - - // fast forward to enable price updates in the below tests - vm.warp(publishTimeInitial + 100); - } - - /** - * @notice getMulticallInfoSearcherContracts function - creates necessary permission and data for multicall to searcher contracts - */ - function getMulticallInfoSearcherContracts( - uint256 vaultNumber, - BidInfo[] memory bidInfos - ) public returns (bytes memory permission, bytes[] memory data) { - vm.roll(2); - - // get permission key - permission = abi.encode( - address(tokenVault), - abi.encodePacked(vaultNumber) - ); - - // raise price of debt token to make vault undercollateralized - bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( - mockPyth, - idsDebt[vaultNumber], - tokenDebtPricesLiqExpressRelay[vaultNumber], - tokenExpo - ); - - data = new bytes[](bidInfos.length); - - for (uint i = 0; i < bidInfos.length; i++) { - // create searcher signature - bytes memory signatureSearcher = createSearcherSignature( - vaultNumber, - bidInfos[i].bid, - bidInfos[i].validUntil, - bidInfos[i].executorSk - ); - data[i] = abi.encodeWithSelector( - searcherA.doLiquidate.selector, - vaultNumber, - bidInfos[i].bid, - bidInfos[i].validUntil, - tokenDebtUpdateData, - signatureSearcher - ); - } - } - - /** - * @notice getMulticallInfoOpportunityAdapter function - creates necessary permission and data for multicall to liquidation adapter contract - */ - function getMulticallInfoOpportunityAdapter( - uint256 vaultNumber, - BidInfo[] memory bidInfos - ) public returns (bytes memory permission, bytes[] memory data) { - vm.roll(2); - - // get permission key - permission = abi.encode( - address(tokenVault), - abi.encodePacked(vaultNumber) - ); - - // raise price of debt token to make vault undercollateralized - bytes[] memory updateDatas = new bytes[](1); - updateDatas[0] = createPriceFeedUpdateSimple( - mockPyth, - idsDebt[vaultNumber], - tokenDebtPricesLiqExpressRelay[vaultNumber], - tokenExpo - ); - - TokenAmount[] memory sellTokens = new TokenAmount[](1); - sellTokens[0] = TokenAmount( - tokensDebt[vaultNumber], - amountsDebt[vaultNumber] - ); - TokenAmount[] memory buyTokens = new TokenAmount[](1); - buyTokens[0] = TokenAmount( - tokensCollateral[vaultNumber], - amountsCollateral[vaultNumber] - ); - - bytes memory calldataVault = abi.encodeWithSelector( - tokenVault.liquidateWithPriceUpdate.selector, - vaultNumber, - updateDatas - ); - - uint256 value = 0; - address contractAddress = address(tokenVault); - - data = new bytes[](bidInfos.length); - - for (uint i = 0; i < bidInfos.length; i++) { - // create liquidation call params struct - bytes - memory signatureLiquidator = createOpportunityExecutionSignature( - sellTokens, - buyTokens, - contractAddress, - calldataVault, - value, - bidInfos[i].bid, - bidInfos[i].validUntil, - bidInfos[i].executorSk - ); - ExecutionParams memory executionParams = ExecutionParams( - sellTokens, - buyTokens, - bidInfos[i].executor, - contractAddress, - calldataVault, - value, - bidInfos[i].validUntil, - bidInfos[i].bid, - signatureLiquidator - ); - - data[i] = abi.encodeWithSelector( - opportunityAdapter.executeOpportunity.selector, - executionParams - ); - } - } - - /** - * @notice assertExpectedBidPayment function - checks that the expected bid payment is equal to the actual bid payment - */ - function assertExpectedBidPayment( - uint256 balancePre, - uint256 balancePost, - BidInfo[] memory bidInfos, - MulticallStatus[] memory multicallStatuses - ) public { - require( - bidInfos.length == multicallStatuses.length, - "bidInfos and multicallStatuses must have the same length" - ); - - uint256 totalBid = 0; - string memory emptyRevertReasonString = ""; - - for (uint i = 0; i < bidInfos.length; i++) { - bool externalSuccess = multicallStatuses[i].externalSuccess; - bool emptyRevertReason = compareStrings( - multicallStatuses[i].multicallRevertReason, - emptyRevertReasonString - ); - - if (externalSuccess && emptyRevertReason) { - totalBid += - (bidInfos[i].bid * feeSplitTokenVault) / - feeSplitPrecisionTokenVault; - } - } - - assertEq(balancePost, balancePre + totalBid); - } - - function testLiquidateNoPER() public { - uint vaultNumber = 0; - // test permissionless liquidation (success) - // raise price of debt token to make vault 0 undercollateralized - bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( - mockPyth, - idsDebt[vaultNumber], - tokenDebtPricesLiqPermissionless[vaultNumber], - tokenExpo - ); - - bytes memory signatureSearcher; - - uint256 validUntil = UINT256_MAX; - - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(searcherAOwnerAddress, searcherAOwnerAddress); - searcherA.doLiquidate( - 0, - 0, - validUntil, - tokenDebtUpdateData, - signatureSearcher - ); - - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq( - balancesAPost.collateral, - balancesAPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesAPost.debt, - balancesAPre.debt - amountsDebt[vaultNumber] - ); - } - - function testLiquidateNoPERFail() public { - uint vaultNumber = 0; - // test permissionless liquidation (failure) - // raise price of debt token to make vault 0 undercollateralized - bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( - mockPyth, - idsDebt[vaultNumber], - tokenDebtPricesLiqExpressRelay[vaultNumber], - tokenExpo - ); - - bytes memory signatureSearcher; - - uint256 validUntil = UINT256_MAX; - - vm.expectRevert(abi.encodeWithSelector(InvalidLiquidation.selector)); - vm.prank(searcherAOwnerAddress, searcherAOwnerAddress); - searcherA.doLiquidate( - 0, - 0, - validUntil, - tokenDebtUpdateData, - signatureSearcher - ); - } - - function testLiquidateSingle() public { - // test ExpressRelay path liquidation (via multicall, express relay operator calls) with searcher contract - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(searcherA); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); - - uint256 balanceProtocolPre = address(tokenVault).balance; - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - uint256 balanceProtocolPost = address(tokenVault).balance; - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq( - balancesAPost.collateral, - balancesAPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesAPost.debt, - balancesAPre.debt - amountsDebt[vaultNumber] - ); - - assertEq(multicallStatuses[0].externalSuccess, true); - - assertExpectedBidPayment( - balanceProtocolPre, - balanceProtocolPost, - bidInfos, - multicallStatuses - ); - } - - /** - * @notice Test a multicall with two calls, where the second is expected to fail - * - * The first call should succeed and liquidate the vault. The second should therefore fail, bc the vault is already liquidated. - */ - function testLiquidateMultipleFailSecond() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](2); - BidInfo[] memory bidInfos = new BidInfo[](2); - - contracts[0] = address(searcherA); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - - contracts[1] = address(searcherB); - bidInfos[1] = makeBidInfo(10, searcherAOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); - - uint256 balanceProtocolPre = address(tokenVault).balance; - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - AccountBalance memory balancesBPre = getBalances( - address(searcherB), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - uint256 balanceProtocolPost = address(tokenVault).balance; - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - AccountBalance memory balancesBPost = getBalances( - address(searcherB), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq( - balancesAPost.collateral, - balancesAPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesAPost.debt, - balancesAPre.debt - amountsDebt[vaultNumber] - ); - - assertEq(balancesBPost.collateral, balancesBPre.collateral); - assertEq(balancesBPost.debt, balancesBPre.debt); - - logMulticallStatuses(multicallStatuses); - - // only the first bid should be paid - assertExpectedBidPayment( - balanceProtocolPre, - balanceProtocolPost, - bidInfos, - multicallStatuses - ); - } - - /** - * @notice Test a multicall with two calls, where the first is expected to fail - * - * The first call should fail, bc the searcher contract has no Eth to pay the express relay. The second should therefore succeed in liquidating the vault. - */ - function testLiquidateMultipleFailFirst() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](2); - BidInfo[] memory bidInfos = new BidInfo[](2); - - contracts[0] = address(searcherA); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - contracts[1] = address(searcherB); - bidInfos[1] = makeBidInfo(10, searcherBOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); - - uint256 balanceProtocolPre = address(tokenVault).balance; - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - AccountBalance memory balancesBPre = getBalances( - address(searcherB), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - // drain searcherA contract of Eth, so that the first liquidation fails - vm.prank(searcherAOwnerAddress, searcherAOwnerAddress); - searcherA.withdrawEth(address(searcherA).balance); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - uint256 balanceProtocolPost = address(tokenVault).balance; - - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - AccountBalance memory balancesBPost = getBalances( - address(searcherB), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq(balancesAPost.collateral, balancesAPre.collateral); - assertEq(balancesAPost.debt, balancesAPre.debt); - - assertEq( - balancesBPost.collateral, - balancesBPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesBPost.debt, - balancesBPre.debt - amountsDebt[vaultNumber] - ); - - logMulticallStatuses(multicallStatuses); - - // only the second bid should be paid - assertExpectedBidPayment( - balanceProtocolPre, - balanceProtocolPost, - bidInfos, - multicallStatuses - ); - } - - function testLiquidateWrongPermission() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(searcherA); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); - - // wrong permisison key - permission = abi.encode(address(0)); - - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq(balancesAPost.collateral, balancesAPre.collateral); - assertEq(balancesAPost.debt, balancesAPre.debt); - - assertFailedExternal(multicallStatuses[0], "InvalidLiquidation()"); - } - - function testLiquidateMismatchedBid() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(searcherA); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); - - // mismatched bid--multicall expects higher bid than what is paid out by the searcher - bidInfos[0].bid = bidInfos[0].bid + 1; - - AccountBalance memory balancesAPre = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - AccountBalance memory balancesAPost = getBalances( - address(searcherA), - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq(balancesAPost.collateral, balancesAPre.collateral); - assertEq(balancesAPost.debt, balancesAPre.debt); - - assertFailedMulticall(multicallStatuses[0], "invalid bid"); - } - - function testLiquidateOpportunityAdapter() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(opportunityAdapter); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); - - AccountBalance memory balancesAPre = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - uint256 balanceProtocolPre = address(tokenVault).balance; - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - uint256 balanceProtocolPost = address(tokenVault).balance; - - AccountBalance memory balancesAPost = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq( - balancesAPost.collateral, - balancesAPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesAPost.debt, - balancesAPre.debt - amountsDebt[vaultNumber] - ); - - assertEq(multicallStatuses[0].externalSuccess, true); - - assertExpectedBidPayment( - balanceProtocolPre, - balanceProtocolPost, - bidInfos, - multicallStatuses - ); - } - - function testLiquidateOpportunityAdapterFailInvalidSignature() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(opportunityAdapter); - bidInfos[0] = makeBidInfo(15, searcherBOwnerSk); - bidInfos[0].executor = searcherAOwnerAddress; // use wrong liquidator address to induce invalid signature - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); - - AccountBalance memory balancesAPre = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - uint256 balanceProtocolPre = address(tokenVault).balance; - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - AccountBalance memory balancesAPost = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - uint256 balanceProtocolPost = address(tokenVault).balance; - - assertEqBalances(balancesAPost, balancesAPre); - assertEq(balanceProtocolPre, balanceProtocolPost); - - assertFailedExternal( - multicallStatuses[0], - "InvalidExecutorSignature()" - ); - } - - function testLiquidateOpportunityAdapterFailExpiredSignature() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](1); - BidInfo[] memory bidInfos = new BidInfo[](1); - - contracts[0] = address(opportunityAdapter); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - bidInfos[0].validUntil = block.timestamp - 1; // use old timestamp for the validUntil field to create expired signature - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); - - AccountBalance memory balancesAPre = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - uint256 balanceProtocolPre = address(tokenVault).balance; - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - AccountBalance memory balancesAPost = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - uint256 balanceProtocolPost = address(tokenVault).balance; - - assertEqBalances(balancesAPost, balancesAPre); - assertEq(balanceProtocolPre, balanceProtocolPost); - assertFailedExternal(multicallStatuses[0], "ExpiredSignature()"); - } - - /** - * @notice Test a multicall with two calls to liquidate the same vault, where the second is expected to fail - * - * The second call should fail with the expected error message, bc the vault is already liquidated. - */ - function testLiquidateLiquidationAdapterFailLiquidationCall() public { - uint256 vaultNumber = 0; - - address[] memory contracts = new address[](2); - BidInfo[] memory bidInfos = new BidInfo[](2); - - contracts[0] = address(opportunityAdapter); - contracts[1] = address(opportunityAdapter); - bidInfos[0] = makeBidInfo(15, searcherAOwnerSk); - bidInfos[1] = makeBidInfo(10, searcherBOwnerSk); - - ( - bytes memory permission, - bytes[] memory data - ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); - - AccountBalance memory balancesAPre = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - AccountBalance memory balancesBPre = getBalances( - searcherBOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - vm.prank(perOperatorAddress, perOperatorAddress); - MulticallStatus[] memory multicallStatuses = expressRelay.multicall( - permission, - contracts, - data, - extractBidAmounts(bidInfos) - ); - - AccountBalance memory balancesAPost = getBalances( - searcherAOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - AccountBalance memory balancesBPost = getBalances( - searcherBOwnerAddress, - tokensCollateral[vaultNumber], - tokensDebt[vaultNumber] - ); - - assertEq( - balancesAPost.collateral, - balancesAPre.collateral + amountsCollateral[vaultNumber] - ); - assertEq( - balancesAPost.debt, - balancesAPre.debt - amountsDebt[vaultNumber] - ); - assertEqBalances(balancesBPost, balancesBPre); - - assertEq(multicallStatuses[0].externalSuccess, true); - assertFailedExternal(multicallStatuses[1], "TargetCallFailed(string)"); - } -} diff --git a/per_multicall/test/ExpressRelayIntegration.t.sol b/per_multicall/test/ExpressRelayIntegration.t.sol new file mode 100644 index 00000000..9d73645a --- /dev/null +++ b/per_multicall/test/ExpressRelayIntegration.t.sol @@ -0,0 +1,650 @@ +// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/Structs.sol"; +import "../src/Errors.sol"; +import "../src/TokenVaultErrors.sol"; + +import {ExpressRelayTestSetup} from "./ExpressRelayTestSetup.sol"; + +/** + * @title ExpressRelayIntegrationTest + * + * ExpressRelayIntegrationTest is a suite that tests the integration of the various contracts in the ExpressRelay stack. + * This includes the ExpressRelay entrypoint contract for all relay interactions, the TokenVault dummy lending protocol contract, individual searcher contracts programmed to perform liquidations, the OpportunityAdapter contract used to facilitate liquidations directly from searcher EOAs, and the relevant token contracts. + * We test the integration of these contracts by creating vaults in the TokenVault protocol, simulating undercollateralization of these vaults to trigger liquidations, constructing the necessary liquidation data, and then calling liquidation through OpportunityAdapter or the searcher contracts. + * + * The focus in these tests is ensuring that liquidation succeeds (or fails as expected) through the ExpressRelay contrct routing to the searcher contracts or the OpportunityAdapter contract. + */ +contract ExpressRelayIntegrationTest is Test, ExpressRelayTestSetup { + /** + * @notice setUp function - sets up the contracts, wallets, tokens, oracle feeds, and vaults for the test + */ + function setUp() public { + setUpWallets(); + setUpContracts(); + setUpTokensAndOracle(); + setUpVaults(); + fundSearcherWallets(); + } + + function testLiquidateNoPER() public { + uint vaultNumber = 0; + // test permissionless liquidation (success) + // raise price of debt token to make vault 0 undercollateralized + bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( + mockPyth, + idsDebt[vaultNumber], + tokenDebtPricesLiqPermissionless[vaultNumber], + tokenExpo + ); + + bytes memory signatureSearcher; + + uint256 validUntil = UINT256_MAX; + + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(searcherAOwnerAddress); + searcherA.doLiquidate( + 0, + 0, + validUntil, + tokenDebtUpdateData, + signatureSearcher + ); + + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq( + balancesAPost.collateral, + balancesAPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesAPost.debt, + balancesAPre.debt - amountsDebt[vaultNumber] + ); + } + + function testLiquidateNoPERFail() public { + uint vaultNumber = 0; + // test permissionless liquidation (failure) + // raise price of debt token to make vault 0 undercollateralized + bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( + mockPyth, + idsDebt[vaultNumber], + tokenDebtPricesLiqExpressRelay[vaultNumber], + tokenExpo + ); + + bytes memory signatureSearcher; + + uint256 validUntil = UINT256_MAX; + + vm.expectRevert(InvalidLiquidation.selector); + vm.prank(searcherAOwnerAddress); + searcherA.doLiquidate( + 0, + 0, + validUntil, + tokenDebtUpdateData, + signatureSearcher + ); + } + + function testLiquidateSingle() public { + // test ExpressRelay path liquidation (via multicall, express relay operator calls) with searcher contract + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(searcherA); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + uint256 balanceProtocolPre = address(tokenVault).balance; + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + uint256 balanceProtocolPost = address(tokenVault).balance; + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq( + balancesAPost.collateral, + balancesAPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesAPost.debt, + balancesAPre.debt - amountsDebt[vaultNumber] + ); + + assertEq(multicallStatuses[0].externalSuccess, true); + + assertExpectedBidPayment( + balanceProtocolPre, + balanceProtocolPost, + bidInfos, + multicallStatuses + ); + } + + /** + * @notice Test a multicall with two calls, where the second is expected to fail + * + * The first call should succeed and liquidate the vault. The second should therefore fail, bc the vault is already liquidated. + */ + function testLiquidateMultipleFailSecond() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](2); + BidInfo[] memory bidInfos = new BidInfo[](2); + + contracts[0] = address(searcherA); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + + contracts[1] = address(searcherB); + bidInfos[1] = makeBidInfo(100, searcherAOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + uint256 balanceProtocolPre = address(tokenVault).balance; + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + AccountBalance memory balancesBPre = getBalances( + address(searcherB), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + uint256 balanceProtocolPost = address(tokenVault).balance; + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + AccountBalance memory balancesBPost = getBalances( + address(searcherB), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq( + balancesAPost.collateral, + balancesAPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesAPost.debt, + balancesAPre.debt - amountsDebt[vaultNumber] + ); + + assertEq(balancesBPost.collateral, balancesBPre.collateral); + assertEq(balancesBPost.debt, balancesBPre.debt); + + logMulticallStatuses(multicallStatuses); + + // only the first bid should be paid + assertExpectedBidPayment( + balanceProtocolPre, + balanceProtocolPost, + bidInfos, + multicallStatuses + ); + } + + /** + * @notice Test a multicall with two calls, where the first is expected to fail + * + * The first call should fail, bc the searcher contract has no Eth to pay the express relay. The second should therefore succeed in liquidating the vault. + */ + function testLiquidateMultipleFailFirst() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](2); + BidInfo[] memory bidInfos = new BidInfo[](2); + + contracts[0] = address(searcherA); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + contracts[1] = address(searcherB); + bidInfos[1] = makeBidInfo(100, searcherBOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + uint256 balanceProtocolPre = address(tokenVault).balance; + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + AccountBalance memory balancesBPre = getBalances( + address(searcherB), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + // drain searcherA contract of Eth, so that the first liquidation fails + vm.prank(searcherAOwnerAddress); + searcherA.withdrawEth(address(searcherA).balance); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + uint256 balanceProtocolPost = address(tokenVault).balance; + + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + AccountBalance memory balancesBPost = getBalances( + address(searcherB), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq(balancesAPost.collateral, balancesAPre.collateral); + assertEq(balancesAPost.debt, balancesAPre.debt); + + assertEq( + balancesBPost.collateral, + balancesBPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesBPost.debt, + balancesBPre.debt - amountsDebt[vaultNumber] + ); + + logMulticallStatuses(multicallStatuses); + + // only the second bid should be paid + assertExpectedBidPayment( + balanceProtocolPre, + balanceProtocolPost, + bidInfos, + multicallStatuses + ); + } + + function testLiquidateWrongPermissionFail() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(searcherA); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + // wrong permisison key + permission = abi.encode(address(0)); + + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq(balancesAPost.collateral, balancesAPre.collateral); + assertEq(balancesAPost.debt, balancesAPre.debt); + + assertFailedExternal(multicallStatuses[0], InvalidLiquidation.selector); + } + + function testLiquidateMismatchedBidFail() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(searcherA); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoSearcherContracts(vaultNumber, bidInfos); + + // mismatched bid--multicall expects higher bid than what is paid out by the searcher + bidInfos[0].bid = bidInfos[0].bid + 1; + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + AccountBalance memory balancesAPre = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + AccountBalance memory balancesAPost = getBalances( + address(searcherA), + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq(balancesAPost.collateral, balancesAPre.collateral); + assertEq(balancesAPost.debt, balancesAPre.debt); + + assertFailedMulticall(multicallStatuses[0], "invalid bid"); + } + + function testLiquidateOpportunityAdapter() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(opportunityAdapter); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + AccountBalance memory balancesAPre = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + uint256 balanceProtocolPre = address(tokenVault).balance; + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + uint256 balanceProtocolPost = address(tokenVault).balance; + + AccountBalance memory balancesAPost = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq( + balancesAPost.collateral, + balancesAPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesAPost.debt, + balancesAPre.debt - amountsDebt[vaultNumber] + ); + + assertEq(multicallStatuses[0].externalSuccess, true); + + assertExpectedBidPayment( + balanceProtocolPre, + balanceProtocolPost, + bidInfos, + multicallStatuses + ); + } + + function testLiquidateOpportunityAdapterInvalidSignatureFail() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(opportunityAdapter); + bidInfos[0] = makeBidInfo(150, searcherBOwnerSk); + bidInfos[0].executor = searcherAOwnerAddress; // use wrong liquidator address to induce invalid signature + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + AccountBalance memory balancesAPre = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + uint256 balanceProtocolPre = address(tokenVault).balance; + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + AccountBalance memory balancesAPost = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + uint256 balanceProtocolPost = address(tokenVault).balance; + + assertEqBalances(balancesAPost, balancesAPre); + assertEq(balanceProtocolPre, balanceProtocolPost); + + assertFailedExternal( + multicallStatuses[0], + InvalidExecutorSignature.selector + ); + } + + function testLiquidateOpportunityAdapterExpiredSignatureFail() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](1); + BidInfo[] memory bidInfos = new BidInfo[](1); + + contracts[0] = address(opportunityAdapter); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + bidInfos[0].validUntil = block.timestamp - 1; // use old timestamp for the validUntil field to create expired signature + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + AccountBalance memory balancesAPre = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + uint256 balanceProtocolPre = address(tokenVault).balance; + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + AccountBalance memory balancesAPost = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + uint256 balanceProtocolPost = address(tokenVault).balance; + + assertEqBalances(balancesAPost, balancesAPre); + assertEq(balanceProtocolPre, balanceProtocolPost); + assertFailedExternal(multicallStatuses[0], ExpiredSignature.selector); + } + + /** + * @notice Test a multicall with two calls to liquidate the same vault, where the second is expected to fail + * + * The second call should fail with the expected error message, bc the vault is already liquidated. + */ + function testLiquidateLiquidationAdapterLiquidationCallFail() public { + uint256 vaultNumber = 0; + + address[] memory contracts = new address[](2); + BidInfo[] memory bidInfos = new BidInfo[](2); + + contracts[0] = address(opportunityAdapter); + contracts[1] = address(opportunityAdapter); + bidInfos[0] = makeBidInfo(150, searcherAOwnerSk); + bidInfos[1] = makeBidInfo(100, searcherBOwnerSk); + + ( + bytes memory permission, + bytes[] memory data + ) = getMulticallInfoOpportunityAdapter(vaultNumber, bidInfos); + + MulticallData[] memory multicallData = getMulticallData( + contracts, + data, + bidInfos + ); + + AccountBalance memory balancesAPre = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + AccountBalance memory balancesBPre = getBalances( + searcherBOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + vm.prank(relayer); + MulticallStatus[] memory multicallStatuses = expressRelay.multicall( + permission, + multicallData + ); + + AccountBalance memory balancesAPost = getBalances( + searcherAOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + AccountBalance memory balancesBPost = getBalances( + searcherBOwnerAddress, + tokensCollateral[vaultNumber], + tokensDebt[vaultNumber] + ); + + assertEq( + balancesAPost.collateral, + balancesAPre.collateral + amountsCollateral[vaultNumber] + ); + assertEq( + balancesAPost.debt, + balancesAPre.debt - amountsDebt[vaultNumber] + ); + assertEqBalances(balancesBPost, balancesBPre); + + assertEq(multicallStatuses[0].externalSuccess, true); + assertFailedExternal(multicallStatuses[1], TargetCallFailed.selector); + } +} diff --git a/per_multicall/test/ExpressRelayTestSetup.sol b/per_multicall/test/ExpressRelayTestSetup.sol new file mode 100644 index 00000000..cd3de4ca --- /dev/null +++ b/per_multicall/test/ExpressRelayTestSetup.sol @@ -0,0 +1,615 @@ +// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved +pragma solidity ^0.8.13; + +import "../src/SigVerify.sol"; +import "forge-std/console.sol"; +import "forge-std/StdMath.sol"; + +import {TokenVault} from "../src/TokenVault.sol"; +import {SearcherVault} from "../src/SearcherVault.sol"; +import {ExpressRelay} from "../src/ExpressRelay.sol"; +import {WETH9} from "../src/WETH9.sol"; +import {OpportunityAdapter} from "../src/OpportunityAdapter.sol"; +import {MyToken} from "../src/MyToken.sol"; +import "../src/Errors.sol"; +import "../src/TokenVaultErrors.sol"; +import "../src/Structs.sol"; + +import "@pythnetwork/pyth-sdk-solidity/MockPyth.sol"; + +import {SafeERC20} from "openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +import "openzeppelin-contracts/contracts/utils/Strings.sol"; + +import "./helpers/Signatures.sol"; +import "./helpers/PriceHelpers.sol"; +import "./helpers/TestParsingHelpers.sol"; +import "./helpers/MulticallHelpers.sol"; +import "../src/OpportunityAdapterUpgradable.sol"; + +/** + * @title ExpressRelayTestSetUp + * + * ExpressRelayTestSetup is a contract that defines set up and helper methods for various test suites. + * + * The set up methods involve creating the necessary contracts and wallets, and initializing the tokens and vaults. + * To create a new suite of tests, the new test contract should inherit from this contract and define its setUp() and test functions. + * Test contracts can derive their setUp() function from setUp... methods defined in this contract. + * + * ExpressRelayTestSetup also defines helper methods that are commonly used in the test suites. + */ +contract ExpressRelayTestSetup is + TestParsingHelpers, + Signatures, + PriceHelpers, + MulticallHelpers +{ + TokenVault public tokenVault; + SearcherVault public searcherA; + SearcherVault public searcherB; + ExpressRelay public expressRelay; + WETH9 public weth; + OpportunityAdapterUpgradable public opportunityAdapter; + MockPyth public mockPyth; + + MyToken public token1; + MyToken public token2; + + bytes32 idToken1; + bytes32 idToken2; + + int32 constant tokenExpo = 0; + + address relayer; + address admin; + address searcherAOwnerAddress; + uint256 searcherAOwnerSk; + address searcherBOwnerAddress; + uint256 searcherBOwnerSk; + address tokenVaultDeployer; + uint256 tokenVaultDeployerSk; + + uint256 constant healthPrecision = 10 ** 16; + + address depositor; // address of the initial depositor into the token vault + + uint256 constant amountToken1DepositorInit = 1_000_000; // amount of token 1 initially owned by the vault depositor + uint256 constant amountToken2DepositorInit = 1_000_000; // amount of token 2 initially owned by the vault depositor + uint256 constant amountToken1AInit = 2_000_000; // amount of token 1 initially owned by searcher A contract + uint256 constant amountToken2AInit = 2_000_000; // amount of token 2 initially owned by searcher A contract + uint256 constant amountToken1BInit = 3_000_000; // amount of token 1 initially owned by searcher B contract + uint256 constant amountToken2BInit = 3_000_000; // amount of token 2 initially owned by searcher B contract + uint256 constant amountToken2TokenVaultInit = 500_000; // amount of token 2 initially owned by the token vault contract (necessary to allow depositor to borrow token 2) + + address[] tokensCollateral; // addresses of collateral, index corresponds to vault number + address[] tokensDebt; // addresses of debt, index corresponds to vault number + uint256[] amountsCollateral; // amounts of collateral, index corresponds to vault number + uint256[] amountsDebt; // amounts of debt, index corresponds to vault number + bytes32[] idsCollateral; // pyth price feed ids of collateral, index corresponds to vault number + bytes32[] idsDebt; // pyth price feed ids of debt, index corresponds to vault number + + // initial token oracle info + int64 constant token1PriceInitial = 100; + uint64 constant token1ConfInitial = 1; + int64 constant token2PriceInitial = 100; + uint64 constant token2ConfInitial = 1; + uint64 constant publishTimeInitial = 1_000_000; + uint64 constant prevPublishTimeInitial = 0; + + int64[] tokenDebtPricesLiqExpressRelay; + int64[] tokenDebtPricesLiqPermissionless; + + // since feeSplitPrecision is set to 10 ** 18, this represents ~50% of the fees + uint256 constant feeSplitProtocolDefault = 50 * 10 ** 16; + // ~5% (10% of the remaining 50%) of the fees go to the relayer + uint256 constant feeSplitRelayer = 10 ** 17; + + uint256 feeSplitTokenVault; + + /** + * @notice setUpWallets function - sets up the wallets for the test + * + * Sets up express relay operator, searcher, initial token vault deployer, and initial vault depositor wallets + */ + function setUpWallets() public { + (relayer, ) = makeAddrAndKey("relayer"); + admin = makeAddr("admin"); + + (searcherAOwnerAddress, searcherAOwnerSk) = makeAddrAndKey("searcherA"); + (searcherBOwnerAddress, searcherBOwnerSk) = makeAddrAndKey("searcherB"); + + (tokenVaultDeployer, tokenVaultDeployerSk) = makeAddrAndKey( + "tokenVaultDeployer" + ); + + (depositor, ) = makeAddrAndKey("depositor"); + } + + /** + * @notice setUpContracts function - sets up the contracts for the test + * + * Sets up the ExpressRelay, WETH9, OpportunityAdapter, MockPyth, TokenVault, SearcherVault, and ERC-20 token contracts + */ + function setUpContracts() public { + // instantiate multicall contract with ExpressRelay operator as the deployer + vm.prank(relayer); + expressRelay = new ExpressRelay( + admin, + relayer, + feeSplitProtocolDefault, + feeSplitRelayer + ); + + vm.prank(relayer); + weth = new WETH9(); + + vm.prank(relayer); + OpportunityAdapterUpgradable _opportunityAdapter = new OpportunityAdapterUpgradable(); + // deploy proxy contract and point it to implementation + ERC1967Proxy proxy = new ERC1967Proxy(address(_opportunityAdapter), ""); + opportunityAdapter = OpportunityAdapterUpgradable(payable(proxy)); + opportunityAdapter.initialize( + // TODO: fix the owner and admin here + relayer, + relayer, + address(expressRelay), + address(weth) + ); + + vm.prank(relayer); + mockPyth = new MockPyth(1_000_000, 0); + + bool allowUndercollateralized = false; + vm.prank(tokenVaultDeployer); // we prank here to standardize the value of the token contract address across different runs + tokenVault = new TokenVault( + address(expressRelay), + address(mockPyth), + allowUndercollateralized + ); + console.log("contract of token vault is", address(tokenVault)); + feeSplitTokenVault = feeSplitProtocolDefault; + + // instantiate searcher A's contract with searcher A's wallet as the deployer + vm.prank(searcherAOwnerAddress); + searcherA = new SearcherVault( + address(expressRelay), + address(tokenVault) + ); + console.log("contract of searcher A is", address(searcherA)); + + // instantiate searcher B's contract with searcher B's wallet as the deployer + vm.prank(searcherBOwnerAddress); + searcherB = new SearcherVault( + address(expressRelay), + address(tokenVault) + ); + console.log("contract of searcher B is", address(searcherB)); + + vm.prank(relayer); + token1 = new MyToken("token1", "T1"); + vm.prank(relayer); + token2 = new MyToken("token2", "T2"); + console.log("contract of token1 is", address(token1)); + console.log("contract of token2 is", address(token2)); + } + + /** + * @notice setUpTokensAndOracle function - sets up the tokens for the test and their initial oracle feeds + * + * Sets up the initial token amounts for the depositor, searcher A, searcher B, and the token vault + * Also sets the initial oracle prices for the tokens + */ + function setUpTokensAndOracle() public { + // mint tokens to the depositor address + token1.mint(depositor, amountToken1DepositorInit); + token2.mint(depositor, amountToken2DepositorInit); + + // mint tokens to searcher A contract + token1.mint(address(searcherA), amountToken1AInit); + token2.mint(address(searcherA), amountToken2AInit); + + // mint tokens to searcher B contract + token1.mint(address(searcherB), amountToken1BInit); + token2.mint(address(searcherB), amountToken2BInit); + + // mint token 2 to the vault contract (to allow creation of initial vault with outstanding debt position) + token2.mint(address(tokenVault), amountToken2TokenVaultInit); + + // create token price feed IDs + idToken1 = bytes32(uint256(uint160(address(token1)))); + idToken2 = bytes32(uint256(uint160(address(token2)))); + + vm.warp(publishTimeInitial); + bytes[] memory updateData = new bytes[](2); + updateData[0] = mockPyth.createPriceFeedUpdateData( + idToken1, + token1PriceInitial, + token1ConfInitial, + tokenExpo, + token1PriceInitial, + token1ConfInitial, + publishTimeInitial, + prevPublishTimeInitial + ); + updateData[1] = mockPyth.createPriceFeedUpdateData( + idToken2, + token2PriceInitial, + token2ConfInitial, + tokenExpo, + token2PriceInitial, + token2ConfInitial, + publishTimeInitial, + prevPublishTimeInitial + ); + + mockPyth.updatePriceFeeds(updateData); + } + + /** + * @notice setUpVaults function - sets up the vaults for the test and stores relevant info per vault + */ + function setUpVaults() public { + // set which tokens are collateral and which are debt for each vault + tokensCollateral = new address[](2); + idsCollateral = new bytes32[](2); + tokensCollateral[0] = address(token1); + idsCollateral[0] = idToken1; + tokensCollateral[1] = address(token1); + idsCollateral[1] = idToken1; + + tokensDebt = new address[](2); + idsDebt = new bytes32[](2); + tokensDebt[0] = address(token2); + idsDebt[0] = idToken2; + tokensDebt[1] = address(token2); + idsDebt[1] = idToken2; + + amountsCollateral = new uint256[](2); + amountsCollateral[0] = 100; + amountsCollateral[1] = 200; + + amountsDebt = new uint256[](2); + amountsDebt[0] = 80; + amountsDebt[1] = 150; + + // create vault 0 + uint256 minCollatPERVault0 = 110 * healthPrecision; + uint256 minCollatPermissionlessVault0 = 100 * healthPrecision; + vm.prank(depositor); + MyToken(tokensCollateral[0]).approve( + address(tokenVault), + amountsCollateral[0] + ); + vm.prank(depositor); + tokenVault.createVault( + tokensCollateral[0], + tokensDebt[0], + amountsCollateral[0], + amountsDebt[0], + minCollatPERVault0, + minCollatPermissionlessVault0, + idsCollateral[0], + idsDebt[0], + new bytes[](0) + ); + + // create vault 1 + uint256 minCollatPERVault1 = 110 * healthPrecision; + uint256 minCollatPermissionlessVault1 = 100 * healthPrecision; + vm.prank(depositor); + MyToken(tokensCollateral[1]).approve( + address(tokenVault), + amountsCollateral[1] + ); + vm.prank(depositor); + tokenVault.createVault( + tokensCollateral[1], + tokensDebt[1], + amountsCollateral[1], + amountsDebt[1], + minCollatPERVault1, + minCollatPermissionlessVault1, + idsCollateral[1], + idsDebt[1], + new bytes[](0) + ); + + int64 priceCollateralVault0; + int64 priceCollateralVault1; + + if (tokensCollateral[0] == address(token1)) { + priceCollateralVault0 = token1PriceInitial; + } else { + priceCollateralVault0 = token2PriceInitial; + } + + int64 tokenDebtPriceLiqPermissionlessVault0; + int64 tokenDebtPriceLiqPERVault0; + int64 tokenDebtPriceLiqPermissionlessVault1; + int64 tokenDebtPriceLiqPERVault1; + + tokenDebtPriceLiqPermissionlessVault0 = getDebtLiquidationPrice( + amountsCollateral[0], + amountsDebt[0], + minCollatPermissionlessVault0, + healthPrecision, + priceCollateralVault0 + ); + + tokenDebtPriceLiqPERVault0 = getDebtLiquidationPrice( + amountsCollateral[0], + amountsDebt[0], + minCollatPERVault0, + healthPrecision, + priceCollateralVault0 + ); + + if (tokensCollateral[1] == address(token1)) { + priceCollateralVault1 = token1PriceInitial; + } else { + priceCollateralVault1 = token2PriceInitial; + } + + tokenDebtPriceLiqPermissionlessVault1 = getDebtLiquidationPrice( + amountsCollateral[1], + amountsDebt[1], + minCollatPermissionlessVault1, + healthPrecision, + priceCollateralVault1 + ); + + tokenDebtPriceLiqPERVault1 = getDebtLiquidationPrice( + amountsCollateral[1], + amountsDebt[1], + minCollatPERVault1, + healthPrecision, + priceCollateralVault1 + ); + + tokenDebtPricesLiqExpressRelay = new int64[](2); + tokenDebtPricesLiqExpressRelay[0] = tokenDebtPriceLiqPERVault0; + tokenDebtPricesLiqExpressRelay[1] = tokenDebtPriceLiqPERVault1; + + tokenDebtPricesLiqPermissionless = new int64[](2); + tokenDebtPricesLiqPermissionless[ + 0 + ] = tokenDebtPriceLiqPermissionlessVault0; + tokenDebtPricesLiqPermissionless[ + 1 + ] = tokenDebtPriceLiqPermissionlessVault1; + } + + /** + * @notice fundSearcherWallets function - funds the searcher wallets with Eth, tokens, and allowances + * + * Funding enables searchers' wallets to directly liquidate via the liquidation adapter + */ + function fundSearcherWallets() public { + // fund searcher A and searcher B + vm.deal(address(searcherA), 1 ether); + vm.deal(address(searcherB), 1 ether); + + address[] memory searchers = new address[](2); + searchers[0] = address(searcherAOwnerAddress); + searchers[1] = address(searcherBOwnerAddress); + + for (uint256 i = 0; i < searchers.length; i++) { + address searcher = searchers[i]; + + // mint tokens to searcher wallet so it can liquidate vaults + MyToken(tokensDebt[0]).mint(address(searcher), amountsDebt[0]); + MyToken(tokensDebt[1]).mint(address(searcher), amountsDebt[1]); + + vm.startPrank(searcher, searcher); + + // create allowance for opportunity adapter + if (tokensDebt[0] == tokensDebt[1]) { + MyToken(tokensDebt[0]).approve( + address(opportunityAdapter), + amountsDebt[0] + amountsDebt[1] + ); + } else { + MyToken(tokensDebt[0]).approve( + address(opportunityAdapter), + amountsDebt[0] + ); + MyToken(tokensDebt[1]).approve( + address(opportunityAdapter), + amountsDebt[1] + ); + } + + // deposit eth into the weth contract + vm.deal(searcher, (i + 1) * 100 ether); + weth.deposit{value: (i + 1) * 100 ether}(); + + // create allowance for opportunity adapter (weth) + weth.approve(address(opportunityAdapter), (i + 1) * 100 ether); + + vm.stopPrank(); + } + + // fast forward to enable price updates in the below tests + vm.warp(publishTimeInitial + 100); + } + + /** + * @notice getMulticallInfoSearcherContracts function - creates necessary permission and data for multicall to searcher contracts + */ + function getMulticallInfoSearcherContracts( + uint256 vaultNumber, + BidInfo[] memory bidInfos + ) public returns (bytes memory permission, bytes[] memory data) { + vm.roll(2); + + // get permission key + permission = abi.encode( + address(tokenVault), + abi.encodePacked(vaultNumber) + ); + + // raise price of debt token to make vault undercollateralized + bytes memory tokenDebtUpdateData = createPriceFeedUpdateSimple( + mockPyth, + idsDebt[vaultNumber], + tokenDebtPricesLiqExpressRelay[vaultNumber], + tokenExpo + ); + + data = new bytes[](bidInfos.length); + + for (uint i = 0; i < bidInfos.length; i++) { + // create searcher signature + bytes memory signatureSearcher = createSearcherSignature( + vaultNumber, + bidInfos[i].bid, + bidInfos[i].validUntil, + bidInfos[i].executorSk + ); + data[i] = abi.encodeWithSelector( + searcherA.doLiquidate.selector, + vaultNumber, + bidInfos[i].bid, + bidInfos[i].validUntil, + tokenDebtUpdateData, + signatureSearcher + ); + } + } + + /** + * @notice getMulticallInfoOpportunityAdapter function - creates necessary permission and data for multicall to liquidation adapter contract + */ + function getMulticallInfoOpportunityAdapter( + uint256 vaultNumber, + BidInfo[] memory bidInfos + ) public returns (bytes memory permission, bytes[] memory data) { + vm.roll(2); + + // get permission key + permission = abi.encode( + address(tokenVault), + abi.encodePacked(vaultNumber) + ); + + // raise price of debt token to make vault undercollateralized + bytes[] memory updateDatas = new bytes[](1); + updateDatas[0] = createPriceFeedUpdateSimple( + mockPyth, + idsDebt[vaultNumber], + tokenDebtPricesLiqExpressRelay[vaultNumber], + tokenExpo + ); + + TokenAmount[] memory sellTokens = new TokenAmount[](1); + sellTokens[0] = TokenAmount( + tokensDebt[vaultNumber], + amountsDebt[vaultNumber] + ); + TokenAmount[] memory buyTokens = new TokenAmount[](1); + buyTokens[0] = TokenAmount( + tokensCollateral[vaultNumber], + amountsCollateral[vaultNumber] + ); + + bytes memory calldataVault = abi.encodeWithSelector( + tokenVault.liquidateWithPriceUpdate.selector, + vaultNumber, + updateDatas + ); + + uint256 value = 0; + address contractAddress = address(tokenVault); + + data = new bytes[](bidInfos.length); + + for (uint i = 0; i < bidInfos.length; i++) { + // create liquidation call params struct + bytes + memory signatureLiquidator = createOpportunityExecutionSignature( + sellTokens, + buyTokens, + contractAddress, + calldataVault, + value, + bidInfos[i].bid, + bidInfos[i].validUntil, + bidInfos[i].executorSk + ); + ExecutionParams memory executionParams = ExecutionParams( + sellTokens, + buyTokens, + bidInfos[i].executor, + contractAddress, + calldataVault, + value, + bidInfos[i].validUntil, + bidInfos[i].bid, + signatureLiquidator + ); + + data[i] = abi.encodeWithSelector( + opportunityAdapter.executeOpportunity.selector, + executionParams + ); + } + } + + function getMulticallData( + address[] memory contracts, + bytes[] memory data, + BidInfo[] memory bidInfos + ) public pure returns (MulticallData[] memory multicallData) { + require( + (contracts.length == data.length) && + (data.length == bidInfos.length), + "contracts, data, and bidAmounts must have the same length" + ); + uint256[] memory bidAmounts = extractBidAmounts(bidInfos); + + multicallData = new MulticallData[](contracts.length); + for (uint i = 0; i < contracts.length; i++) { + multicallData[i] = MulticallData( + contracts[i], + data[i], + bidAmounts[i] + ); + } + } + + /** + * @notice assertExpectedBidPayment function - checks that the expected bid payment is equal to the actual bid payment + */ + function assertExpectedBidPayment( + uint256 balancePre, + uint256 balancePost, + BidInfo[] memory bidInfos, + MulticallStatus[] memory multicallStatuses + ) public { + require( + bidInfos.length == multicallStatuses.length, + "bidInfos and multicallStatuses must have the same length" + ); + + uint256 totalBid = 0; + string memory emptyRevertReasonString = ""; + + for (uint i = 0; i < bidInfos.length; i++) { + bool externalSuccess = multicallStatuses[i].externalSuccess; + bool emptyRevertReason = compareStrings( + multicallStatuses[i].multicallRevertReason, + emptyRevertReasonString + ); + + if (externalSuccess && emptyRevertReason) { + totalBid += + (bidInfos[i].bid * feeSplitTokenVault) / + expressRelay.getFeeSplitPrecision(); + } + } + + assertEq(balancePost, balancePre + totalBid); + } +} diff --git a/per_multicall/test/ExpressRelayUnit.t.sol b/per_multicall/test/ExpressRelayUnit.t.sol new file mode 100644 index 00000000..98326c15 --- /dev/null +++ b/per_multicall/test/ExpressRelayUnit.t.sol @@ -0,0 +1,164 @@ +// Copyright (C) 2024 Lavra Holdings Limited - All Rights Reserved +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/Errors.sol"; +import "../src/Structs.sol"; + +import {ExpressRelayTestSetup} from "./ExpressRelayTestSetup.sol"; + +/** + * @title ExpressRelayUnitTest + * + * ExpressRelayUnitTest is a suite that tests the ExpressRelay contract. + * This relates to testing the ExpressRelay setter methods and multicall. + */ +contract ExpressRelayUnitTest is Test, ExpressRelayTestSetup { + function setUp() public { + setUpWallets(); + setUpContracts(); + } + + function testSetRelayerByAdmin() public { + address newRelayer = makeAddr("newRelayer"); + vm.prank(admin); + expressRelay.setRelayer(newRelayer); + + assertEq(expressRelay.getRelayer(), newRelayer); + } + + function testSetRelayerByNonAdminFail() public { + address newRelayer = makeAddr("newRelayer"); + vm.expectRevert(Unauthorized.selector); + vm.prank(relayer); + expressRelay.setRelayer(newRelayer); + } + + function testSetFeeProtocolDefaultByAdmin() public { + uint256 feeSplitProtocolDefaultPre = expressRelay + .getFeeProtocolDefault(); + uint256 fee = feeSplitProtocolDefaultPre + 1; + vm.prank(admin); + expressRelay.setFeeProtocolDefault(fee); + uint256 feeSplitProtocolDefaultPost = expressRelay + .getFeeProtocolDefault(); + + assertEq(feeSplitProtocolDefaultPost, feeSplitProtocolDefaultPre + 1); + } + + function testSetFeeProtocolDefaultByAdminHighFail() public { + // test setting default fee to the highest valid value + uint256 feeMax = 10 ** 18; + vm.prank(admin); + expressRelay.setFeeProtocolDefault(feeMax); + uint256 feeProtocolDefaultPost = expressRelay.getFeeProtocolDefault(); + assertEq(feeProtocolDefaultPost, feeMax); + + // test setting default fee to a value higher than the highest valid value, should fail + uint256 feeInvalid = 10 ** 18 + 1; + vm.expectRevert(InvalidFeeSplit.selector); + vm.prank(admin); + expressRelay.setFeeProtocolDefault(feeInvalid); + } + + function testSetFeeProtocolDefaultByNonAdminFail() public { + vm.expectRevert(Unauthorized.selector); + vm.prank(relayer); + expressRelay.setFeeProtocolDefault(0); + } + + function testGetFeeSplitProtocolUncustomized() public { + address protocol = makeAddr("protocol"); + uint256 feeSplitProtocolDefaultPre = expressRelay + .getFeeProtocolDefault(); + uint256 feeSplitProtocol = expressRelay.getFeeProtocol(protocol); + assertEq(feeSplitProtocol, feeSplitProtocolDefaultPre); + } + + function testSetFeeProtocolByAdmin() public { + address protocol = makeAddr("protocol"); + + uint256 feeProtocolPre = expressRelay.getFeeProtocol(protocol); + uint256 fee = feeProtocolPre + 1; + vm.prank(admin); + expressRelay.setFeeProtocol(protocol, fee); + uint256 feeProtocolPost = expressRelay.getFeeProtocol(protocol); + + assertEq(feeProtocolPost, feeProtocolPre + 1); + } + + function testSetFeeProtocolByAdminHighFail() public { + address protocol = makeAddr("protocol"); + + // test setting fee to the highest valid value + uint256 feeMax = 10 ** 18; + vm.prank(admin); + expressRelay.setFeeProtocol(protocol, feeMax); + uint256 feeProtocolPost = expressRelay.getFeeProtocol(protocol); + assertEq(feeProtocolPost, feeMax); + + // test setting fee to a value higher than the highest valid value, should fail + uint256 feeInvalid = 10 ** 18 + 1; + vm.expectRevert(InvalidFeeSplit.selector); + vm.prank(admin); + expressRelay.setFeeProtocol(protocol, feeInvalid); + } + + function testSetFeeProtocolByNonAdminFail() public { + address protocol = makeAddr("protocol"); + + vm.expectRevert(Unauthorized.selector); + vm.prank(relayer); + expressRelay.setFeeProtocol(protocol, 0); + } + + function testSetFeeRelayerByAdmin() public { + uint256 feeSplitRelayerPre = expressRelay.getFeeRelayer(); + uint256 fee = feeSplitRelayerPre + 1; + vm.prank(admin); + expressRelay.setFeeRelayer(fee); + uint256 feeSplitRelayerPost = expressRelay.getFeeRelayer(); + + assertEq(feeSplitRelayerPre, feeSplitRelayer); + assertEq(feeSplitRelayerPost, feeSplitRelayerPre + 1); + } + + function testSetFeeRelayerByAdminHighFail() public { + // test setting fee to the highest valid value + uint256 feeMax = 10 ** 18; + vm.prank(admin); + expressRelay.setFeeRelayer(feeMax); + uint256 feeRelayerPost = expressRelay.getFeeRelayer(); + + // test setting fee to a value higher than the highest valid value, should fail + uint256 fee = 10 ** 18 + 1; + vm.expectRevert(InvalidFeeSplit.selector); + vm.prank(admin); + expressRelay.setFeeRelayer(fee); + } + + function testSetFeeRelayerByNonAdminFail() public { + vm.expectRevert(Unauthorized.selector); + vm.prank(relayer); + expressRelay.setFeeRelayer(0); + } + + function testMulticallByRelayerEmpty() public { + bytes memory permission = abi.encode("random permission"); + MulticallData[] memory multicallData; + + vm.prank(relayer); + expressRelay.multicall(permission, multicallData); + } + + function testMulticallByNonRelayerFail() public { + bytes memory permission = abi.encode("random permission"); + MulticallData[] memory multicallData; + + vm.expectRevert(Unauthorized.selector); + vm.prank(address(0xbad)); + expressRelay.multicall(permission, multicallData); + } +} diff --git a/per_multicall/test/helpers/MulticallHelpers.sol b/per_multicall/test/helpers/MulticallHelpers.sol index 97a93f8e..ff0e6131 100644 --- a/per_multicall/test/helpers/MulticallHelpers.sol +++ b/per_multicall/test/helpers/MulticallHelpers.sol @@ -17,12 +17,9 @@ contract MulticallHelpers is Test, TestParsingHelpers { function assertFailedExternal( MulticallStatus memory status, - string memory reason + bytes4 errorSelector ) internal { - assertEq( - abi.encodePacked(bytes4(status.externalResult)), - keccakHash(reason) - ); + assertEq(bytes4(status.externalResult), errorSelector); } function logMulticallStatuses(