diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..a035630 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index abbfe30..4f8dfd0 100644 --- a/.gitignore +++ b/.gitignore @@ -20,13 +20,13 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# Hardhat files +# Hardhat files (legacy) /cache /artifacts deployments/hardhat/ deployments/localhost/ -# TypeChain files +# TypeChain files (legacy) /typechain /typechain-types @@ -43,6 +43,11 @@ gasReporterOutput.json ignition/deployments/ !ignition/deployments/.gitkeep +# Foundry files +/out +/cache_forge +broadcast/ + # In-Dev fuelVM fuelVM/vault/tests fuelVM/vault/out diff --git a/.gitmodules b/.gitmodules index c6997b2..f27e450 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,9 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/openzeppelin-contracts-upgradeable"] + path = lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable [submodule "lib/openzeppelin-foundry-upgrades"] path = lib/openzeppelin-foundry-upgrades url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/.openzeppelin/base-sepolia.json b/.openzeppelin/base-sepolia.json new file mode 100644 index 0000000..33b63e1 --- /dev/null +++ b/.openzeppelin/base-sepolia.json @@ -0,0 +1,217 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x82A45a6Cbe14B2707a3b375Cf0aA5Eb74c95A5bB", + "txHash": "0xc848a5efaf0eba01ef973a5f755ccb496f40ab23ae8e1b442d51e02978f8cef9", + "kind": "uups" + }, + { + "address": "0x2ba677A74505d30fB88c48a4Ea27889e6C5e1C0f", + "txHash": "0xc9a2228dab7ad832291d4550b6ecb15d990d6fd336fa3fd44a8ec6d09d7c5722", + "kind": "uups" + }, + { + "address": "0x8A822d8c6CaB0AD28bacC205DacBaeB926CD9AFA", + "txHash": "0xb3e482e7eba611d71edee41d6c7a9f231974ecf1faf4d429452e73d58d43d9a4", + "kind": "uups" + }, + { + "address": "0xA7458040272226378397C3036eda862D60C3b307", + "txHash": "0x8e0e423f8d63023cbe1afde242cb31e31284d1050cbeda2d25670ccce389e80f", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0x8fC21F3E8e73F8d626a7B809Ae58d492258d64D0", + "txHash": "0x85a3d3f42dba20e26083b5d7ba42474093a2ab51040e1a6388f2c69c5f8f7820", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/.openzeppelin/op-sepolia.json b/.openzeppelin/op-sepolia.json new file mode 100644 index 0000000..452e0cf --- /dev/null +++ b/.openzeppelin/op-sepolia.json @@ -0,0 +1,202 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x10B69f0E3c21C1187526940A615959E9ee6012F9", + "txHash": "0x9773fd0eb347665a4d0de8bfcf5abb71a1657c196d21b788187983c7eaf4944e", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0x107CdC100BB330aDc1Bf2B4F5682d44Cf251cA63", + "txHash": "0x1b6f2eed7e8831253b6a92a82f581e2a6bd24a97b645142231eb708d5da3f8bd", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/.openzeppelin/sepolia.json b/.openzeppelin/sepolia.json new file mode 100644 index 0000000..cd12778 --- /dev/null +++ b/.openzeppelin/sepolia.json @@ -0,0 +1,212 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x3B7C0E49c607d47f4711D8573312eA5B6480566D", + "txHash": "0xb0c4a2c20883bd4c3e18255fe907b5446e68d71ce4dddaa170e8b937c1b7ab70", + "kind": "uups" + }, + { + "address": "0xCd744a840ab0E6A30dC05f99410B3e0282579f90", + "txHash": "0xe7f5e9179d9e21fd69d233d306288ab1a5d0ddbe0c53e81a3b6b6e446468af7c", + "kind": "uups" + }, + { + "address": "0xd579B76e3f51884c50eB8e8efdef5C593666B8fb", + "txHash": "0x920af821970a9acbe7694a4945d19496f44b2ede52b0676829c76b8b72826a37", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0xa627da4951D9c7EE8bde2674e6853e485b791FF9", + "txHash": "0xac0e3dbf5a160a99115f752fa52dcfac6f1242e31a5ac31cc8fb8b034b482fb5", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/.openzeppelin/unknown-421614.json b/.openzeppelin/unknown-421614.json new file mode 100644 index 0000000..72aaaaa --- /dev/null +++ b/.openzeppelin/unknown-421614.json @@ -0,0 +1,202 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x10B69f0E3c21C1187526940A615959E9ee6012F9", + "txHash": "0xf447466d983237a420178d8208a40eec81a80cc27306567b3f5a310fc46e294b", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0x107CdC100BB330aDc1Bf2B4F5682d44Cf251cA63", + "txHash": "0x5265ce114bfbd7ca0873c72faf52c8a17fd6542800c8f973c578e6b2998dcc57", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/.openzeppelin/unknown-4326.json b/.openzeppelin/unknown-4326.json new file mode 100644 index 0000000..b191001 --- /dev/null +++ b/.openzeppelin/unknown-4326.json @@ -0,0 +1,202 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x10B69f0E3c21C1187526940A615959E9ee6012F9", + "txHash": "0x016c696a0ea5debe2891ec3bec6474b7718f9af3930372d820726518714f318e", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0x107CdC100BB330aDc1Bf2B4F5682d44Cf251cA63", + "txHash": "0x5d4636353de093b3abed99d8d10525074251be643a590f5b67927783ceaa25fc", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/.openzeppelin/unknown-5115.json b/.openzeppelin/unknown-5115.json new file mode 100644 index 0000000..7bef92c --- /dev/null +++ b/.openzeppelin/unknown-5115.json @@ -0,0 +1,217 @@ +{ + "manifestVersion": "3.2", + "proxies": [ + { + "address": "0x10B69f0E3c21C1187526940A615959E9ee6012F9", + "txHash": "0x76f6c4f984b665c4a30ce4f8fdae2a45124ecdd2951c9b4badcd5c41e2e4aca6", + "kind": "uups" + }, + { + "address": "0xAc73E77b4FE9BBAAA35C7147DC3Fd5286929A746", + "txHash": "0xe2b22cd6b2ef52969f67cd4dac5b599c6c617bc50fb9db6bf179fbf942ff49f6", + "kind": "uups" + }, + { + "address": "0x590cb8868c6DeBc12CCd42E837042659cfB91504", + "txHash": "0xfc49522fd7a4e3ef32978ecd59b76842090d5abef825fcfa746b1a562f90c126", + "kind": "uups" + }, + { + "address": "0x91BC4bd9Ced9cD9C35467a0797a0724A3FA7ff9b", + "txHash": "0x6f9593769e2bfe0eee5580f4447abbf03835b08b08ef800917cc3c2fd676e5c1", + "kind": "uups" + } + ], + "impls": { + "c2b19447dcaeab360db983727b6a9aabdcd834afb5e62c7ce412c18de69a3e4d": { + "address": "0x107CdC100BB330aDc1Bf2B4F5682d44Cf251cA63", + "txHash": "0x46a9e41fd03553199aecd26e5e86dc6fa4ac146188e40eaff72b6f3ac1e96b59", + "layout": { + "solcVersion": "0.8.30", + "storage": [ + { + "label": "requestState", + "offset": 0, + "slot": "0", + "type": "t_mapping(t_bytes32,t_enum(RFFState)3527)", + "contract": "Vault", + "src": "contracts/Vault.sol:48" + }, + { + "label": "winningSolver", + "offset": 0, + "slot": "1", + "type": "t_mapping(t_bytes32,t_address)", + "contract": "Vault", + "src": "contracts/Vault.sol:49" + }, + { + "label": "depositNonce", + "offset": 0, + "slot": "2", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:50" + }, + { + "label": "fillNonce", + "offset": 0, + "slot": "3", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:51" + }, + { + "label": "settleNonce", + "offset": 0, + "slot": "4", + "type": "t_mapping(t_uint256,t_bool)", + "contract": "Vault", + "src": "contracts/Vault.sol:52" + }, + { + "label": "__gap", + "offset": 0, + "slot": "5", + "type": "t_array(t_uint256)50_storage", + "contract": "Vault", + "src": "contracts/Vault.sol:57" + } + ], + "types": { + "t_address": { + "label": "address", + "numberOfBytes": "20" + }, + "t_bool": { + "label": "bool", + "numberOfBytes": "1" + }, + "t_bytes32": { + "label": "bytes32", + "numberOfBytes": "32" + }, + "t_mapping(t_address,t_bool)": { + "label": "mapping(address => bool)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_struct(RoleData)27_storage)": { + "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)", + "numberOfBytes": "32" + }, + "t_struct(AccessControlStorage)37_storage": { + "label": "struct AccessControlUpgradeable.AccessControlStorage", + "members": [ + { + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "offset": 0, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(InitializableStorage)518_storage": { + "label": "struct Initializable.InitializableStorage", + "members": [ + { + "label": "_initialized", + "type": "t_uint64", + "offset": 0, + "slot": "0" + }, + { + "label": "_initializing", + "type": "t_bool", + "offset": 8, + "slot": "0" + } + ], + "numberOfBytes": "32" + }, + "t_struct(RoleData)27_storage": { + "label": "struct AccessControlUpgradeable.RoleData", + "members": [ + { + "label": "hasRole", + "type": "t_mapping(t_address,t_bool)", + "offset": 0, + "slot": "0" + }, + { + "label": "adminRole", + "type": "t_bytes32", + "offset": 0, + "slot": "1" + } + ], + "numberOfBytes": "64" + }, + "t_uint64": { + "label": "uint64", + "numberOfBytes": "8" + }, + "t_array(t_uint256)50_storage": { + "label": "uint256[50]", + "numberOfBytes": "1600" + }, + "t_enum(RFFState)3527": { + "label": "enum Vault.RFFState", + "members": [ + "UNPROCESSED", + "DEPOSITED", + "FULFILLED" + ], + "numberOfBytes": "1" + }, + "t_mapping(t_bytes32,t_address)": { + "label": "mapping(bytes32 => address)", + "numberOfBytes": "32" + }, + "t_mapping(t_bytes32,t_enum(RFFState)3527)": { + "label": "mapping(bytes32 => enum Vault.RFFState)", + "numberOfBytes": "32" + }, + "t_mapping(t_uint256,t_bool)": { + "label": "mapping(uint256 => bool)", + "numberOfBytes": "32" + }, + "t_uint256": { + "label": "uint256", + "numberOfBytes": "32" + } + }, + "namespaces": { + "erc7201:openzeppelin.storage.AccessControl": [ + { + "contract": "AccessControlUpgradeable", + "label": "_roles", + "type": "t_mapping(t_bytes32,t_struct(RoleData)27_storage)", + "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:62", + "offset": 0, + "slot": "0" + } + ], + "erc7201:openzeppelin.storage.Initializable": [ + { + "contract": "Initializable", + "label": "_initialized", + "type": "t_uint64", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:69", + "offset": 0, + "slot": "0" + }, + { + "contract": "Initializable", + "label": "_initializing", + "type": "t_bool", + "src": "@openzeppelin/contracts/proxy/utils/Initializable.sol:73", + "offset": 8, + "slot": "0" + } + ] + } + } + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..670659a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,252 @@ +# AGENTS.md - Arcana Credit Smart Contracts + +## Project Overview + +Arcana Credit is a cross-chain intent settlement protocol. The Vault contract manages deposits, fulfillments, and settlements. Users sign messages authorizing fund movements; backend servers execute transactions on their behalf. + +**Tech Stack:** +- Solidity ^0.8.29/^0.8.30 (EVM contracts) +- Foundry (primary) + Hardhat (secondary) build systems +- OpenZeppelin Contracts v5.x (upgradeable + standard) +- Sway/FuelVM (auxiliary Fuel blockchain implementation) + +## Build Commands + +### Foundry (Primary) +```bash +# Build +forge build + +# Run all tests +forge test + +# Run single test file +forge test --match-path test/Vault.t.sol + +# Run single test function +forge test --match-test test_Deposit_ERC20Tokens + +# Run tests with verbosity +forge test -vvvv + +# Run tests with gas report +forge test --gas-report + +# Format code +forge fmt + +# Check formatting +forge fmt --check +``` + +### Hardhat (Secondary) +```bash +# Build +npm run compile +# or: npx hardhat compile + +# Run tests +npm test +# or: npx hardhat test + +# Deploy (multi-network) +npm run deploy:multi + +# Upgrade proxies +npm run upgrade:multi + +# Estimate gas +npm run estimate-gas +``` + +### FuelVM (Sway) +```bash +cd fuelVM/vault +forc build +``` + +## Code Style Guidelines + +### File Structure +``` +src/ + ├── Vault.sol # Main upgradeable vault + ├── Router.sol # Cross-chain router + ├── ERC20Sweeper.sol # Token sweeper utility + ├── types.sol # Shared structs/enums + ├── interfaces/ # Contract interfaces + └── routes/ # Route implementations (mayan.sol) +test/ + └── *.t.sol # Foundry tests +script/ + └── Deploy*.s.sol # Foundry deploy scripts +scripts/ + └── *.js # Hardhat JS scripts +``` + +### Solidity Conventions + +**License & Pragma:** +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; +``` + +**Imports - Use named imports with curly braces:** +```solidity +// Good +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// Bad - avoid wildcard imports +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +``` + +**Formatting (foundry.toml [fmt]):** +- Line length: 100 characters +- Tab width: 4 spaces +- Bracket spacing: enabled +- Int types: `long` (uint256, not uint) +- Multiline function header: `attributes_first` +- Quote style: double quotes +- Number underscore: thousands (1_000_000) + +**Naming Conventions:** +- Constants: `SCREAMING_SNAKE_CASE` (e.g., `UPGRADER_ROLE`) +- Private variables: `camelCase` with leading underscore for internal helpers +- Events: `PascalCase` (e.g., `Deposit`, `Fulfilment`, `Settle`) +- Errors: `PascalCase` (e.g., `InvalidRoute`, `ZeroAddress`) +- Functions: `camelCase` +- Structs/Enums: `PascalCase` + +**NatSpec Documentation:** +```solidity +/// @title Contract title +/// @author Author name (@handle) +/// @notice User-facing description +/// @dev Technical implementation details +contract Example { + /// @notice Emitted when X happens + /// @param param1 Description + event SomeEvent(uint256 indexed param1); + + /// @notice Function description + /// @param param Description + /// @return Description of return value + function example(uint256 param) external returns (uint256) { } +} +``` + +### Error Handling + +**Prefer custom errors over require strings for gas efficiency:** +```solidity +// Good - custom errors (gas efficient) +error InvalidRoute(); +error ZeroAddress(); +if (routerAddress == address(0)) revert ZeroAddress(); + +// Acceptable - require with string (used in existing code) +require(success, "Vault: Invalid signature or from"); +``` + +**Revert message format:** `"ContractName: Error description"` + +### Upgradeable Contracts Pattern + +```solidity +contract Vault is + Initializable, + UUPSUpgradeable, + AccessControlUpgradeable, + ReentrancyGuardTransient +{ + // Storage gap for future upgrades + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address admin) public initializer { + __AccessControl_init(); + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + function _authorizeUpgrade(address newImplementation) + internal + override + onlyRole(UPGRADER_ROLE) + { } +} +``` + +### Test Conventions (Foundry) + +```solidity +import { Test } from "forge-std/Test.sol"; + +contract VaultTest is Test { + function setUp() public { + // Setup code + } + + // Test naming: test_FunctionName_Scenario + function test_Deposit_ERC20Tokens() public { } + function test_Deposit_RevertsOnExpiredRequest() public { } + + // Fuzz tests + function test_SweepERC20_Fuzz(uint256 amount) public { + vm.assume(amount > 0 && amount < type(uint128).max); + } +} +``` + +**Test helpers:** +- `makeAddr("name")` - Create labeled address +- `vm.prank(addr)` - Next call from addr +- `vm.deal(addr, amount)` - Set ETH balance +- `vm.sign(pk, hash)` - Sign message +- `vm.expectRevert("message")` - Expect revert +- `vm.createSelectFork("network")` - Fork mainnet + +### Security Patterns + +1. **Reentrancy:** Use `nonReentrant` modifier or `ReentrancyGuardTransient` +2. **Safe transfers:** Always use `SafeERC20` for token transfers +3. **Fee-on-transfer tokens:** Verify actual received amount +4. **Signature verification:** Use OpenZeppelin's `ECDSA.recover()` +5. **Nonce management:** Track used nonces to prevent replay attacks +6. **Access control:** Use role-based access (`AccessControl`) + +### Key Remappings +``` +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +forge-std/=lib/forge-std/src/ +``` + +### Environment Variables +Required in `.env`: +- `PRIVATE_KEY` - Deployer private key +- `*_RPC_URL` - Network RPC endpoints (ETHEREUM, POLYGON, ARBITRUM, etc.) +- `COINMARKETCAP_API_KEY` - For gas reporting +- `*SCAN_API_KEY` - Block explorer API keys + +### Common Patterns in Codebase + +**bytes32 to address conversion:** +```solidity +function bytes32ToAddress(bytes32 a) internal pure returns (address) { + return address(uint160(uint256(a))); +} +``` + +**EIP-191 signed message verification:** +```solidity +bytes32 signedMessageHash = keccak256( + abi.encodePacked("\x19Ethereum Signed Message:\n32", _hashRequest(request)) +); +address signer = signedMessageHash.recover(signature); +``` diff --git a/foundry.lock b/foundry.lock index 3fb0b96..5295cc1 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,8 +1,17 @@ { "lib/forge-std": { "tag": { - "name": "v1.12.0", - "rev": "7117c90c8cf6c68e5acce4f09a6b24715cea4de6" + "name": "v1.14.0", + "rev": "1801b0541f4fda118a10798fd3486bb7051c5dd6" } + }, + "lib/openzeppelin-contracts": { + "rev": "fcbae5394ae8ad52d8e580a3477db99814b9d565" + }, + "lib/openzeppelin-contracts-upgradeable": { + "rev": "aa677e9d28ed78fc427ec47ba2baef2030c58e7c" + }, + "lib/openzeppelin-foundry-upgrades": { + "rev": "cbce1e00305e943aa1661d43f41e5ac72c662b07" } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index be069f3..7a9eaca 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,8 @@ src = "contracts" out = "out" libs = ["node_modules", "lib"] remappings = [ - "@openzeppelin/=node_modules/@openzeppelin/", + "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/", + "@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/", "forge-std/=lib/forge-std/src/", "createx-forge/=lib/createx-forge/", ] @@ -23,8 +24,6 @@ arbitrum = "${ARBITRUM_ONE_RPC_URL}" optimism = "${OPTIMISM_MAINNET_RPC_URL}" base = "${BASE_MAINNET_RPC_URL}" scroll = "${SCROLL_MAINNET_RPC_URL}" -linea = "${LINEA_MAINNET_RPC_URL}" -sophon = "${SOPHON_MAINNET_RPC_URL}" avalanche = "${AVALANCHE_C_CHAIN_RPC_URL}" hyperliquid = "${HYPERLIQUID_RPC_URL}" kaia = "${KAIA_MAINNET_RPC_URL}" diff --git a/hardhat.config.ts b/hardhat.config.ts index 1def582..107624c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,3 +1,4 @@ + import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; import "@openzeppelin/hardhat-upgrades"; @@ -112,6 +113,22 @@ const config: HardhatUserConfig = { url: process.env.SEPOLIA, accounts: [PK], }, + sepolia: { + url: process.env.SEPOLIA_RPC_URL!, + accounts: [PK], + }, + base_sepolia: { + url: process.env.BASE_SEPOLIA_RPC_URL!, + accounts: [PK], + }, + polygon_amony: { + url: process.env.POLYGON_AMOY_RPC_URL!, + accounts: [PK], + }, + mega_eth: { + url: process.env.MEGA_ETH_RPC_URL!, + accounts: [PK], + } }, gasReporter: { enabled: true, @@ -119,6 +136,28 @@ const config: HardhatUserConfig = { coinmarketcap: process.env.COINMARKETCAP_API_KEY!, excludeContracts: [], }, + paths: { + sources: "./src", + tests: "./test", + cache: "./cache", + artifacts: "./artifacts" + + }, + etherscan: { + apiKey: { + 'mega-mainnet': 'empty' + }, + customChains: [ + { + network: "mega-mainnet", + chainId: 4326, + urls: { + apiURL: "https://megaeth.blockscout.com/api", + browserURL: "https://megaeth.blockscout.com" + } + } + ] + } }; export default config; diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..aa677e9 --- /dev/null +++ b/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c diff --git a/remappings.txt b/remappings.txt index 09f58e1..2e59203 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,4 @@ -@openzeppelin/=node_modules/@openzeppelin/ +@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ forge-std/=lib/forge-std/src/ createx-forge/=lib/createx-forge/ diff --git a/script/DeployAll.s.sol b/script/DeployAll.s.sol new file mode 100644 index 0000000..fc811cb --- /dev/null +++ b/script/DeployAll.s.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Router} from "../src/Router.sol"; +import {Vault} from "../src/Vault.sol"; +import {MayanRouter} from "../src/routes/mayan.sol"; +import {Route} from "../src/types.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; + +interface ICreateX { + function deployCreate2(bytes32 salt, bytes memory initCode) external payable returns (address); + function computeCreate2Address(bytes32 salt, bytes32 initCodeHash) external view returns (address); +} + +/// @title DeployAll +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Script to deploy complete system via createX deterministic CREATE2 +contract DeployAll is Script { + ICreateX public constant CREATEX = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); + + struct DeploymentAddresses { + address router; + address vaultProxy; + address mayanRouter; + address admin; + } + + function run() external returns (DeploymentAddresses memory addresses) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address admin = vm.envAddress("ADMIN_ADDRESS"); + + addresses.admin = admin; + + vm.startBroadcast(deployerPrivateKey); + address deployer = vm.addr(deployerPrivateKey); + + console.log("\n========== Deploying Router (createX) =========="); + bytes32 routerSalt = keccak256(abi.encodePacked("nexus-mayan-router-1.0.5")); + bytes memory routerInitCode = abi.encodePacked(type(Router).creationCode, abi.encode(deployer)); + addresses.router = CREATEX.deployCreate2(routerSalt, routerInitCode); + console.log("Router deployed at:", addresses.router); + + console.log("\n========== Deploying Vault (createX) =========="); + bytes32 vaultSalt = keccak256(abi.encodePacked("nexus-mayan-vault-1.0.5")); + bytes32 proxySalt = keccak256(abi.encodePacked(vaultSalt, "proxy")); + + bytes memory vaultInitCode = type(Vault).creationCode; + bytes32 vaultInitCodeHash = keccak256(vaultInitCode); + address expectedImpl = CREATEX.computeCreate2Address(keccak256(abi.encode(vaultSalt)), vaultInitCodeHash); + console.log("Expected implementation:", expectedImpl); + + bytes memory initData = abi.encodeWithSelector(Vault.initialize.selector, deployer); + bytes memory proxyInitCode = + abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(expectedImpl, initData)); + bytes32 proxyInitCodeHash = keccak256(proxyInitCode); + address expectedProxy = CREATEX.computeCreate2Address(keccak256(abi.encode(proxySalt)), proxyInitCodeHash); + console.log("Expected proxy:", expectedProxy); + + address implementation = CREATEX.deployCreate2(vaultSalt, vaultInitCode); + console.log("Implementation:", implementation); + require(implementation == expectedImpl, "Implementation address mismatch"); + + addresses.vaultProxy = CREATEX.deployCreate2(proxySalt, proxyInitCode); + console.log("Vault proxy:", addresses.vaultProxy); + require(addresses.vaultProxy == expectedProxy, "Proxy address mismatch"); + + Vault vault = Vault(payable(addresses.vaultProxy)); + vault.setRouter(addresses.router); + console.log("Router set in Vault"); + + // If admin is different, transfer control + if (addresses.admin != deployer) { + vault.grantRole(vault.DEFAULT_ADMIN_ROLE(), addresses.admin); + vault.renounceRole(vault.DEFAULT_ADMIN_ROLE(), deployer); + console.log("Transferred Vault admin rights to:", addresses.admin); + } + + console.log("\n========== Deploying MayanRouter (createX) =========="); + bytes32 mayanSalt = keccak256(abi.encodePacked("nexus-mayan-mayanrouter-1.0.5")); + bytes memory mayanInitCode = abi.encodePacked(type(MayanRouter).creationCode, abi.encode(deployer)); + addresses.mayanRouter = CREATEX.deployCreate2(mayanSalt, mayanInitCode); + console.log("MayanRouter:", addresses.mayanRouter); + + Router router = Router(addresses.router); + router.setRouter(Route.MAYAN, addresses.mayanRouter); + console.log("MayanRouter configured in Router"); + + MayanRouter mayanRouter = MayanRouter(addresses.mayanRouter); + if (addresses.admin != deployer) { + mayanRouter.transferOwnership(addresses.admin); + console.log("Transferred MayanRouter ownership to:", addresses.admin); + } + + // Transfer Router admin rights if needed + if (addresses.admin != deployer) { + router.grantRole(router.DEFAULT_ADMIN_ROLE(), addresses.admin); + router.renounceRole(router.DEFAULT_ADMIN_ROLE(), deployer); + console.log("Transferred Router admin rights to:", addresses.admin); + } + + vm.stopBroadcast(); + + console.log("\n========== Verifying Deployment =========="); + _verifyDeployment(addresses); + + _printSummary(addresses); + + return addresses; + } + + function _verifyDeployment(DeploymentAddresses memory addresses) internal view { + Router router = Router(addresses.router); + Vault vault = Vault(payable(addresses.vaultProxy)); + + require(router.hasRole(router.DEFAULT_ADMIN_ROLE(), addresses.admin), "Router: Admin role not granted"); + require(router.routers(Route.MAYAN) == addresses.mayanRouter, "Router: MayanRouter not configured"); + + require(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), addresses.admin), "Vault: Admin role not granted"); + require(address(vault.router()) == addresses.router, "Vault: Router not set"); + + console.log("All verifications passed"); + } + + function _printSummary(DeploymentAddresses memory addresses) internal pure { + console.log("\n========================================"); + console.log("DEPLOYMENT SUMMARY"); + console.log("========================================"); + console.log("Admin:", addresses.admin); + console.log("----------------------------------------"); + console.log("Router:", addresses.router); + console.log("----------------------------------------"); + console.log("Vault Proxy:", addresses.vaultProxy); + console.log("----------------------------------------"); + console.log("MayanRouter:", addresses.mayanRouter); + console.log("========================================\n"); + } +} diff --git a/script/DeployERC20Mock.s.sol b/script/DeployERC20Mock.s.sol new file mode 100644 index 0000000..994475f --- /dev/null +++ b/script/DeployERC20Mock.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC20Mock} from "../src/mocks/ERC20Mock.sol"; + +contract DeployERC20Mock is Script { + function run() external returns (address token) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerPrivateKey); + + ERC20Mock mockToken = new ERC20Mock(); + token = address(mockToken); + + vm.stopBroadcast(); + + console.log("ERC20Mock deployed at:", token); + console.log("Name: Mock Token"); + console.log("Symbol: MOCK"); + + return token; + } +} diff --git a/script/DeployMayanRouter.s.sol b/script/DeployMayanRouter.s.sol new file mode 100644 index 0000000..a875fe4 --- /dev/null +++ b/script/DeployMayanRouter.s.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {MayanRouter} from "../src/routes/mayan.sol"; +import {Router} from "../src/Router.sol"; +import {Route} from "../src/types.sol"; + +/// @title DeployMayanRouter +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Script to deploy MayanRouter and configure it in the Router +contract DeployMayanRouter is Script { + function run() external returns (address mayanRouter) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address routerProxyAddress = vm.envAddress("ROUTER_ADDRESS"); + + require(routerProxyAddress != address(0), "Router address cannot be zero"); + + vm.startBroadcast(deployerPrivateKey); + + address deployer = vm.addr(deployerPrivateKey); + + // Deploy MayanRouter + mayanRouter = address(new MayanRouter(deployer)); + console.log("MayanRouter deployed at:", mayanRouter); + + // Configure Router to use MayanRouter + Router router = Router(routerProxyAddress); + router.setRouter(Route.MAYAN, mayanRouter); + console.log("MayanRouter set in Router for Route.MAYAN"); + + address admin = vm.envAddress("ADMIN_ADDRESS"); + + MayanRouter mayanRouterContract = MayanRouter(mayanRouter); + if (admin != deployer) { + mayanRouterContract.transferOwnership(admin); + console.log("Transferred MayanRouter ownership to:", admin); + } + + vm.stopBroadcast(); + + // Verify configuration + require(router.routers(Route.MAYAN) == mayanRouter, "MayanRouter not set correctly"); + console.log("Deployment and configuration verified successfully"); + + return mayanRouter; + } +} diff --git a/script/DeployRouter.s.sol b/script/DeployRouter.s.sol new file mode 100644 index 0000000..432e926 --- /dev/null +++ b/script/DeployRouter.s.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Script} from "forge-std/Script.sol"; +import {console} from "forge-std/console.sol"; +import {Router} from "../src/Router.sol"; + +/// @title DeployRouter +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Script to deploy the Router contract (non-upgradeable) +contract DeployRouter is Script { + function run() external returns (address router) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address admin = vm.envAddress("ADMIN_ADDRESS"); + + vm.startBroadcast(deployerPrivateKey); + address deployer = vm.addr(deployerPrivateKey); + + // Deploy Router directly (no proxy) + // Initialize with deployer to allow configuration if needed + Router routerContract = new Router(deployer); + router = address(routerContract); + + console.log("Router deployed at:", router); + console.log("Admin address:", admin); + + if (admin != deployer) { + routerContract.grantRole(routerContract.DEFAULT_ADMIN_ROLE(), admin); + routerContract.renounceRole(routerContract.DEFAULT_ADMIN_ROLE(), deployer); + console.log("Transferred Router admin rights to:", admin); + } + + vm.stopBroadcast(); + + // Verify deployment + require(routerContract.hasRole(routerContract.DEFAULT_ADMIN_ROLE(), admin), "Admin role not granted"); + console.log("Deployment verified successfully"); + + return router; + } +} diff --git a/script/DeployVaultCreateX.s.sol b/script/DeployVaultCreateX.s.sol index 6446a8a..c6468d7 100644 --- a/script/DeployVaultCreateX.s.sol +++ b/script/DeployVaultCreateX.s.sol @@ -12,12 +12,12 @@ interface ICreateX { contract DeployVault is Script { ICreateX public constant CREATEX = ICreateX(0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed); - bytes32 public constant DEFAULT_SALT = keccak256("nexus-vault-1.0.1"); + bytes32 public constant DEFAULT_SALT = keccak256("nexus-vault-1.0.5"); bytes32 private constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); function run() external returns (address proxy) { address admin = _getAdmin(); - address finalAdmin = _getFinalAdmin(); + address finalAdmin = _getFinalAdmin(admin); vm.startBroadcast(); bytes32 salt = _getSalt(); bytes32 proxySalt = keccak256(abi.encodePacked(salt, "proxy")); @@ -53,14 +53,14 @@ contract DeployVault is Script { // Transfer ownership to finalAdmin if specified and different from admin if (finalAdmin != address(0) && finalAdmin != admin) { - _transferOwnership(proxy, finalAdmin); + _transferOwnership(proxy, admin, finalAdmin); } } - function _transferOwnership(address proxy, address finalAdmin) internal { + function _transferOwnership(address proxy, address admin, address finalAdmin) internal { uint256 adminPrivateKey = _getAdminPrivateKey(); require(adminPrivateKey != 0, "DeployVault: ADMIN_PRIVATE_KEY required for ownership transfer"); - address upgraderWallet = _getUpgraderWallet(); + address upgraderWallet = _getUpgraderWallet(admin); vm.startBroadcast(adminPrivateKey); @@ -70,10 +70,9 @@ contract DeployVault is Script { vault.grantRole(vault.DEFAULT_ADMIN_ROLE(), finalAdmin); console.log("Granted DEFAULT_ADMIN_ROLE to:", finalAdmin); - // Grant UPGRADER_ROLE to upgrader wallet (or finalAdmin if not set) - address upgrader = upgraderWallet != address(0) ? upgraderWallet : finalAdmin; - vault.grantRole(UPGRADER_ROLE, upgrader); - console.log("Granted UPGRADER_ROLE to:", upgrader); + // Grant UPGRADER_ROLE to upgrader wallet (defaults to admin if not set) + vault.grantRole(UPGRADER_ROLE, upgraderWallet); + console.log("Granted UPGRADER_ROLE to:", upgraderWallet); // Get the deployer address (current admin) address deployer = vm.addr(adminPrivateKey); @@ -119,11 +118,11 @@ contract DeployVault is Script { } } - function _getFinalAdmin() internal view returns (address) { + function _getFinalAdmin(address admin) internal view returns (address) { try vm.envAddress("FINAL_ADMIN") returns (address envFinalAdmin) { return envFinalAdmin; } catch { - return address(0); + return admin; } } @@ -135,11 +134,11 @@ contract DeployVault is Script { } } - function _getUpgraderWallet() internal view returns (address) { + function _getUpgraderWallet(address admin) internal view returns (address) { try vm.envAddress("UPGRADER_WALLET") returns (address envUpgrader) { return envUpgrader; } catch { - return address(0); + return admin; } } } diff --git a/script/GrantRole.s.sol b/script/GrantRole.s.sol index aa73b0f..791b86b 100644 --- a/script/GrantRole.s.sol +++ b/script/GrantRole.s.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.29; import {Script, console} from "forge-std/Script.sol"; -import {Vault} from "../contracts/Vault.sol"; interface IVault { function hasRole(bytes32 role, address account) external view returns (bool); diff --git a/script/UpgradeVault.s.sol b/script/UpgradeVault.s.sol index 98329ef..d13e769 100644 --- a/script/UpgradeVault.s.sol +++ b/script/UpgradeVault.s.sol @@ -41,7 +41,7 @@ contract UpgradeVault is Script { function _upgrade(address proxyAddress, bytes32 salt) internal { address deployer; - + // Try to use PRIVATE_KEY from env, otherwise use AWS KMS (via --aws flag) try vm.envUint("PRIVATE_KEY") returns (uint256 deployerPrivateKey) { deployer = vm.addr(deployerPrivateKey); diff --git a/scripts/deploy-proxy-multi-network.js b/scripts/deploy-proxy-multi-network.js index fcb0be2..b2f0312 100644 --- a/scripts/deploy-proxy-multi-network.js +++ b/scripts/deploy-proxy-multi-network.js @@ -137,7 +137,6 @@ function runMultiNetwork(adminAddress, networks) { try { const adminArg = adminAddress ? `--admin ${adminAddress}` : ""; const cmd = `npx hardhat run scripts/deploy-proxy-multi-network.js --network ${networkName} ${adminArg}`; - const output = execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], diff --git a/scripts/deploy-vault-multi-network.js b/scripts/deploy-vault-multi-network.js index ccdba9c..4ef7221 100644 --- a/scripts/deploy-vault-multi-network.js +++ b/scripts/deploy-vault-multi-network.js @@ -32,6 +32,11 @@ const NATIVE_TOKENS = { kaia_mainnet: "KAI", bnb_smart_chain_mainnet: "BNB", monad_mainnet: "MONAD", + sepolia: "ETH", + "arb_sepolia": "ETH", + "op_sepolia": "ETH", + "base_sepolia": "ETH", + "polygon_amony": "MATIC", }; // Deployment results storage diff --git a/contracts/ERC20Sweeper.sol b/src/ERC20Sweeper.sol similarity index 100% rename from contracts/ERC20Sweeper.sol rename to src/ERC20Sweeper.sol diff --git a/src/Router.sol b/src/Router.sol new file mode 100644 index 0000000..bbe453d --- /dev/null +++ b/src/Router.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; + +import {Request, Route} from "./types.sol"; +import {ICaRouter} from "./interfaces/ICaRouter.sol"; + +/// @title Router +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Main routing contract for cross-chain transfers via multiple route providers +/// @dev Non-upgradeable contract with role-based access control +contract Router is AccessControl { + using SafeERC20 for IERC20; + using ECDSA for bytes32; + + /// @notice Emitted when a transfer is routed via native protocol + /// @param requestHash Hash of the action request + event NativeRoute(bytes32 indexed requestHash); + + /// @notice Emitted when a transfer is routed via Mayan protocol + /// @param requestHash Hash of the action request + event MayanRoute(bytes32 indexed requestHash); + + /// @notice Emitted when a router address is updated + /// @param route Route identifier + /// @param routerAddress New router contract address + event RouterSet(Route indexed route, address indexed routerAddress); + + /// @notice Thrown when an invalid or unconfigured route is requested + error InvalidRoute(); + + /// @notice Thrown when a zero address is provided where not allowed + error ZeroAddress(); + + /// @notice Mapping of route identifiers to router contract addresses + mapping(Route => address) public routers; + + /// @notice Initialize the router contract with admin + /// @param admin Address to grant DEFAULT_ADMIN_ROLE + constructor(address admin) { + _grantRole(DEFAULT_ADMIN_ROLE, admin); + } + + /// @notice Set or update a router contract address for a specific route + /// @dev Only callable by admin + /// @param route Route identifier (NATIVE or MAYAN) + /// @param routerAddress Address of the router contract implementing ICaRouter + function setRouter(Route route, address routerAddress) external onlyRole(DEFAULT_ADMIN_ROLE) { + if (routerAddress == address(0)) revert ZeroAddress(); + routers[route] = routerAddress; + emit RouterSet(route, routerAddress); + } + + /// @notice Process a cross-chain transfer via the specified route + /// @dev Delegates to the appropriate router contract + /// @param request Action struct containing transfer details + /// @param route Route to use (NATIVE or MAYAN) + /// @param data Additional route-specific encoded parameters + function processTransfer(Request calldata request, Route route, bytes calldata data) external payable { + address routerAddress = routers[route]; + if (routerAddress == address(0)) revert InvalidRoute(); + + (uint256 chainIndex,) = abi.decode(data, (uint256, bytes)); + + address tokenAddress = address(uint160(uint256(request.sources[chainIndex].contractAddress))); + if (tokenAddress != address(0)) { + IERC20(tokenAddress).approve(routerAddress, request.sources[chainIndex].value); + } + + ICaRouter router = ICaRouter(routerAddress); + if (route == Route.MAYAN) { + bytes32 requestHash = keccak256(abi.encode(request)); + emit MayanRoute(requestHash); + } else { + revert InvalidRoute(); + } + + router.processTransfer{value: msg.value}(request, data); + } +} diff --git a/contracts/USDC.sol b/src/USDC.sol similarity index 100% rename from contracts/USDC.sol rename to src/USDC.sol diff --git a/src/Vault.sol b/src/Vault.sol new file mode 100644 index 0000000..ae6b475 --- /dev/null +++ b/src/Vault.sol @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; +import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +import {Request, Party, Universe, RFFState, SettleData, Route} from "./types.sol"; +import {IRouter} from "./interfaces/IRouter.sol"; + +/// @title Vault +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Vault contract for managing deposits, fulfillments, and settlements of cross-chain transfers +/// @dev UUPS upgradeable contract with role-based access control +contract Vault is Initializable, UUPSUpgradeable, AccessControlUpgradeable, ReentrancyGuardTransient { + using ECDSA for bytes32; + using SafeERC20 for IERC20; + + mapping(bytes32 => RFFState) public requestState; + mapping(bytes32 => address) public winningSolver; + mapping(uint256 => bool) public depositNonce; + mapping(uint256 => bool) public fillNonce; + mapping(uint256 => bool) public settleNonce; + + /// @notice Router contract for processing cross-chain transfers + IRouter public router; + + bytes32 private constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + bytes32 private constant SETTLEMENT_VERIFIER_ROLE = keccak256("SETTLEMENT_VERIFIER_ROLE"); + string private constant SIGNATURE_PREFIX = "Sign this intent to proceed \n"; + // Storage gap to reserve slots for future use + uint256[50] private __gap; + + event Deposit(bytes32 indexed requestHash, address from); + event Fulfilment(bytes32 indexed requestHash, address from, address solver); + event Settle(uint256 indexed nonce, address[] solver, address[] token, uint256[] amount); + event RouterSet(address indexed newRouter); + event DepositAndRoute(bytes32 indexed requestHash, address from, Route route); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address admin) public initializer { + __AccessControl_init(); + + _grantRole(DEFAULT_ADMIN_ROLE, admin); + _grantRole(UPGRADER_ROLE, admin); + } + + /// @notice Set the router contract address + /// @param _router Address of the Router contract + function setRouter(address _router) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(_router != address(0), "Vault: Zero address"); + router = IRouter(_router); + emit RouterSet(_router); + } + + function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) {} + + function version() external pure returns (string memory) { + return "1.0.0"; + } + + function _hashRequest(Request calldata request) private pure returns (bytes32) { + return keccak256( + abi.encode( + request.sources, + request.destinationUniverse, + request.destinationChainID, + request.recipientAddress, + request.destinations, + request.nonce, + request.expiry, + request.parties + ) + ); + } + + function bytes32ToAddress(bytes32 a) internal pure returns (address) { + // Cast the last 20 bytes of bytes32 into an address + return address(uint160(uint256(a))); + } + + function _verify_request(bytes calldata signature, address from, bytes32 hash) + private + pure + returns (bool, bytes32) + { + // Must match EXACT client string: "Sign this intent to proceed \n" + "0x...." + bytes memory msgBytes = abi.encodePacked( + SIGNATURE_PREFIX, + Strings.toHexString(uint256(hash), 32) // 0x + 64 hex chars + ); + + // EIP-191 hash with dynamic decimal length (e.g. 95) + bytes32 signedMessageHash = MessageHashUtils.toEthSignedMessageHash(msgBytes); + + address signer = signedMessageHash.recover(signature); + return (signer == from, signedMessageHash); + } + + function deposit(Request calldata request, bytes calldata signature, uint256 chainIndex) + external + payable + nonReentrant + { + address from = extractAddress(request.parties); + bytes32 request_hash = _hashRequest(request); + (bool success, bytes32 signedMessageHash) = _verify_request(signature, from, request_hash); + require(success, "Vault: Invalid signature or from"); + require(request.sources[chainIndex].chainID == block.chainid, "Vault: Chain ID mismatch"); + require(request.sources[chainIndex].universe == Universe.ETHEREUM, "Vault: Universe mismatch"); + require(!depositNonce[request.nonce], "Vault: Nonce already used"); + require(request.expiry > block.timestamp, "Vault: Request expired"); + + depositNonce[request.nonce] = true; + requestState[signedMessageHash] = RFFState.DEPOSITED; + + if (request.sources[chainIndex].contractAddress == bytes32(0)) { + uint256 totalValue = request.sources[chainIndex].value; + require(msg.value == totalValue, "Vault: Value mismatch"); + } else { + IERC20 token = IERC20(bytes32ToAddress(request.sources[chainIndex].contractAddress)); + + uint256 bal = token.balanceOf(address(this)); + token.safeTransferFrom(from, address(this), request.sources[chainIndex].value); + // fee on transfer tokens + if (token.balanceOf(address(this)) - bal != request.sources[chainIndex].value) { + revert("Vault: failed to transfer the source amount"); + } + + if (request.sources[chainIndex].fee > 0 && msg.sender != from) { + uint256 solverBal = token.balanceOf(msg.sender); + token.safeTransferFrom(from, msg.sender, request.sources[chainIndex].fee); + if (token.balanceOf(msg.sender) - solverBal != request.sources[chainIndex].fee) { + revert("Vault: failed to transfer the fee amount"); + } + } else if (request.sources[chainIndex].fee > 0 && msg.sender == from) { + revert("Vault: self-fee transfer not allowed"); + } + } + + emit Deposit(request_hash, from); + } + + /// @notice Deposit funds and initiate cross-chain transfer via router + /// @param request Request struct for router containing cross-chain transfer details + /// @param signature User's signature authorizing the deposit + /// @param chainIndex Index of the source chain in the request.sources array + /// @param route Route to use (NATIVE or MAYAN) + /// @param routeData Additional route-specific encoded parameters + function depositRouter( + Request calldata request, + bytes calldata signature, + uint256 chainIndex, + Route route, + bytes calldata routeData + ) external payable nonReentrant { + require(address(router) != address(0), "Vault: Router not set"); + require(chainIndex < request.destinations.length, "Vault: Invalid destination index"); + + address from = extractAddress(request.parties); + bytes32 request_hash = _hashRequest(request); + (bool success, bytes32 requestHash) = _verify_request(signature, from, request_hash); + require(success, "Vault: Invalid signature or from"); + require(request.sources[chainIndex].chainID == block.chainid, "Vault: Chain ID mismatch"); + require(request.sources[chainIndex].universe == Universe.ETHEREUM, "Vault: Universe mismatch"); + require(!depositNonce[request.nonce], "Vault: Nonce already used"); + require(request.expiry > block.timestamp, "Vault: Request expired"); + + depositNonce[request.nonce] = true; + requestState[requestHash] = RFFState.DEPOSITED; + + uint256 valueToRoute = 0; + + if (request.sources[chainIndex].contractAddress == bytes32(0)) { + uint256 totalValue = request.sources[chainIndex].value; + require(msg.value == totalValue, "Vault: Value mismatch"); + valueToRoute = totalValue; + } else { + IERC20 token = IERC20(bytes32ToAddress(request.sources[chainIndex].contractAddress)); + + uint256 bal = token.balanceOf(address(this)); + token.safeTransferFrom(from, address(this), request.sources[chainIndex].value); + + if (token.balanceOf(address(this)) - bal != request.sources[chainIndex].value) { + revert("Vault: failed to transfer the source amount"); + } + + token.safeTransfer(address(router), request.sources[chainIndex].value); + + if (request.sources[chainIndex].fee > 0 && msg.sender != from) { + uint256 solverBal = token.balanceOf(msg.sender); + token.safeTransferFrom(from, msg.sender, request.sources[chainIndex].fee); + if (token.balanceOf(msg.sender) - solverBal != request.sources[chainIndex].fee) { + revert("Vault: failed to transfer the fee amount"); + } + } else if (request.sources[chainIndex].fee > 0 && msg.sender == from) { + revert("Vault: self-fee transfer not allowed"); + } + } + + bytes memory encodedRouteData = abi.encode(chainIndex, routeData); + router.processTransfer{value: valueToRoute}(request, route, encodedRouteData); + + emit DepositAndRoute(request_hash, from, route); + } + + function extractAddress(Party[] memory parties) internal pure returns (address user) { + for (uint256 i = 0; i < parties.length; ++i) { + if (parties[i].universe == Universe.ETHEREUM) { + return bytes32ToAddress(parties[i].address_); + } + } + revert("Vault: Party not found"); + } + + function fulfil(Request calldata request, bytes calldata signature) external payable nonReentrant { + address from = extractAddress(request.parties); + bytes32 request_hash = _hashRequest(request); + (bool success, bytes32 signedMessageHash) = _verify_request(signature, from, request_hash); + require(success, "Vault: Invalid signature or from"); + require(uint256(request.destinationChainID) == block.chainid, "Vault: Chain ID mismatch"); + require(request.destinationUniverse == Universe.ETHEREUM, "Vault: Universe mismatch"); + require(!fillNonce[request.nonce], "Vault: Nonce already used"); + require(request.expiry > block.timestamp, "Vault: Request expired"); + address recipient = bytes32ToAddress(request.recipientAddress); + + fillNonce[request.nonce] = true; + requestState[signedMessageHash] = RFFState.FULFILLED; + winningSolver[signedMessageHash] = msg.sender; + + uint256 nativeBalance = msg.value; + for (uint256 i = 0; i < request.destinations.length; ++i) { + if (request.destinations[i].contractAddress == bytes32(0)) { + require(nativeBalance >= request.destinations[i].value, "Vault: Value mismatch"); + require(request.destinations[i].value > 0, "Vault: Value mismatch"); + nativeBalance -= request.destinations[i].value; + (bool sent,) = payable(recipient).call{value: request.destinations[i].value}(""); + require(sent, "Vault: Transfer failed"); + } else { + IERC20 token = IERC20(bytes32ToAddress(request.destinations[i].contractAddress)); + + uint256 bal = token.balanceOf(recipient); + token.safeTransferFrom(msg.sender, recipient, request.destinations[i].value); + // fee on transfer tokens + if (token.balanceOf(recipient) - bal != request.destinations[i].value) { + revert("Vault: failed to transfer the destination amount"); + } + } + } + if (nativeBalance > 0) { + (bool sent,) = payable(msg.sender).call{value: nativeBalance}(""); + require(sent, "Vault: Transfer failed"); + } + emit Fulfilment(request_hash, from, msg.sender); + } + + function settle(SettleData calldata settleData, bytes calldata signature) external nonReentrant { + bytes32 structHash = keccak256( + abi.encode( + settleData.universe, + settleData.chainID, + settleData.vaultAddress, + settleData.solvers, + settleData.contractAddresses, + settleData.amounts, + settleData.nonce + ) + ); + bytes32 signatureHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", structHash)); + address signer = signatureHash.recover(signature); + require(hasRole(SETTLEMENT_VERIFIER_ROLE, signer), "Vault: Invalid signature"); + require(settleData.solvers.length == settleData.contractAddresses.length, "tokens length mismatch"); + + require(settleData.solvers.length == settleData.amounts.length, "amounts length mismatch"); + require(!settleNonce[settleData.nonce], "Vault: Nonce already used"); + require(settleData.chainID == block.chainid, "Vault: Chain ID mismatch"); + require(settleData.universe == Universe.ETHEREUM, "Vault: Universe mismatch"); + require(settleData.vaultAddress == address(this), "Vault: Invalid vault address"); + + settleNonce[settleData.nonce] = true; + for (uint256 i = 0; i < settleData.solvers.length; ++i) { + if (settleData.contractAddresses[i] == address(0)) { + (bool sent,) = settleData.solvers[i].call{value: settleData.amounts[i]}(""); + require(sent, "Vault: Transfer failed"); + } else { + IERC20 token = IERC20(settleData.contractAddresses[i]); + token.safeTransfer(settleData.solvers[i], settleData.amounts[i]); + } + } + emit Settle(settleData.nonce, settleData.solvers, settleData.contractAddresses, settleData.amounts); + } +} diff --git a/src/interfaces/ICaRouter.sol b/src/interfaces/ICaRouter.sol new file mode 100644 index 0000000..c96d524 --- /dev/null +++ b/src/interfaces/ICaRouter.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Request} from "../types.sol"; + +interface ICaRouter { + function processTransfer(Request calldata request, bytes calldata data) external payable; +} diff --git a/src/interfaces/IMayanForwarder.sol b/src/interfaces/IMayanForwarder.sol new file mode 100644 index 0000000..6e7fa43 --- /dev/null +++ b/src/interfaces/IMayanForwarder.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +/// @title IMayanForwarder +/// @notice Interface for Mayan's forwarder contract that handles token transfers +interface IMayanForwarder { + /// @notice Permit parameters for gasless ERC20 approvals + struct PermitParams { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + /// @notice Forward ERC20 tokens to Mayan protocol + /// @param tokenIn Source token address + /// @param amountIn Amount to forward + /// @param permitParams Permit signature parameters + /// @param mayanProtocol Target Mayan protocol address + /// @param protocolData Encoded protocol call data + function forwardERC20( + address tokenIn, + uint256 amountIn, + PermitParams calldata permitParams, + address mayanProtocol, + bytes calldata protocolData + ) external payable; + + /// @notice Forward native ETH to Mayan protocol + /// @param mayanProtocol Target Mayan protocol address + /// @param protocolData Encoded protocol call data + function forwardEth(address mayanProtocol, bytes calldata protocolData) external payable; + + /// @notice Swap tokens and forward native ETH to Mayan protocol + /// @param amountIn Amount of ETH to swap + /// @param swapProtocol Address of the swap protocol + /// @param swapData Encoded swap data for the swap protocol + /// @param middleToken Address of the expected middle token + /// @param minMiddleAmount Minimum amount of middle token to receive + /// @param mayanProtocol Target Mayan protocol address + /// @param mayanData Encoded protocol call data for Mayan + function swapAndForwardEth( + uint256 amountIn, + address swapProtocol, + bytes calldata swapData, + address middleToken, + uint256 minMiddleAmount, + address mayanProtocol, + bytes calldata mayanData + ) external payable; +} diff --git a/src/interfaces/IMayanSwiftV1.sol b/src/interfaces/IMayanSwiftV1.sol new file mode 100644 index 0000000..c1de0cf --- /dev/null +++ b/src/interfaces/IMayanSwiftV1.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +/// @title IMayanSwiftV1 +/// @notice Interface for Mayan Swift V1 cross-chain swap protocol +interface IMayanSwiftV1 { + /// @notice Order parameters for cross-chain swap + struct OrderParams { + bytes32 trader; + bytes32 tokenOut; + uint64 minAmountOut; + uint64 gasDrop; + uint64 cancelFee; + uint64 refundFee; + uint64 deadline; + bytes32 destAddr; + uint16 destChainId; + bytes32 referrerAddr; + uint8 referrerBps; + uint8 auctionMode; + bytes32 random; + } + + /// @notice Create order with native ETH + /// @param params Order parameters + /// @return orderHash Hash of the created order + function createOrderWithEth(OrderParams memory params) external payable returns (bytes32 orderHash); + + /// @notice Create order with ERC20 token + /// @param tokenIn Source token address + /// @param amountIn Amount to swap + /// @param params Order parameters + /// @return orderHash Hash of the created order + function createOrderWithToken(address tokenIn, uint256 amountIn, OrderParams memory params) + external + returns (bytes32 orderHash); +} diff --git a/src/interfaces/IMayanSwiftV2.sol b/src/interfaces/IMayanSwiftV2.sol new file mode 100644 index 0000000..51292a5 --- /dev/null +++ b/src/interfaces/IMayanSwiftV2.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +/// @title IMayanSwiftV2 +/// @notice Interface for Mayan Swift V2 cross-chain swap protocol +interface IMayanSwiftV2 { + /// @notice Order parameters for cross-chain swap + struct OrderParams { + uint8 payloadType; + bytes32 trader; + bytes32 destAddr; + uint16 destChainId; + bytes32 referrerAddr; + bytes32 tokenOut; + uint64 minAmountOut; + uint64 gasDrop; + uint64 cancelFee; + uint64 refundFee; + uint64 deadline; + uint8 referrerBps; + uint8 auctionMode; + bytes32 random; + } + + /// @notice Create order with ERC20 token + /// @param tokenIn Source token address + /// @param amountIn Amount to swap + /// @param params Order parameters + /// @param customPayload Additional payload data + /// @return orderHash Hash of the created order + function createOrderWithToken( + address tokenIn, + uint256 amountIn, + OrderParams memory params, + bytes memory customPayload + ) external returns (bytes32 orderHash); +} diff --git a/src/interfaces/IRouter.sol b/src/interfaces/IRouter.sol new file mode 100644 index 0000000..d21ae28 --- /dev/null +++ b/src/interfaces/IRouter.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Request, Route} from "../types.sol"; + +/// @title IRouter +/// @notice Interface for the main Router contract +interface IRouter { + /// @notice Process a cross-chain transfer via the specified route + /// @param request Action struct containing transfer details + /// @param route Route to use (NATIVE or MAYAN) + /// @param data Additional route-specific encoded parameters + function processTransfer(Request calldata request, Route route, bytes calldata data) external payable; + + /// @notice Set or update a router contract address for a specific route + /// @param route Route identifier + /// @param routerAddress Address of the router contract + function setRouter(Route route, address routerAddress) external; + + /// @notice Get the router address for a specific route + /// @param route Route identifier + /// @return Router contract address + function routers(Route route) external view returns (address); +} diff --git a/src/mocks/ERC20Mock.sol b/src/mocks/ERC20Mock.sol new file mode 100644 index 0000000..5c286d7 --- /dev/null +++ b/src/mocks/ERC20Mock.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +contract ERC20Mock is ERC20, ERC20Permit { + constructor() ERC20("Mock Token", "MOCK") ERC20Permit("Mock Token") { + _mint(msg.sender, 1_000_000_000 * 10 ** decimals()); + } + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} diff --git a/src/routes/mayan.sol b/src/routes/mayan.sol new file mode 100644 index 0000000..29b3af2 --- /dev/null +++ b/src/routes/mayan.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Request, Universe, SourcePair, Party} from "../types.sol"; +import {ICaRouter} from "../interfaces/ICaRouter.sol"; +import {IMayanSwiftV1} from "../interfaces/IMayanSwiftV1.sol"; +import {IMayanForwarder} from "../interfaces/IMayanForwarder.sol"; +import {IMayanSwiftV2} from "../interfaces/IMayanSwiftV2.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +enum SwiftVersion { + V2, + V1 +} + +/// @title MayanRouter +/// @author Rachit Anand Srivastava (@privacy_prophet) +/// @notice Router contract for Mayan Swift V2 cross-chain swaps via Wormhole +/// @dev Implements ICaRouter for integration with Arcana Credit protocol +contract MayanRouter is ICaRouter, Ownable { + address public constant MAYAN_FORWARDER = 0x337685fdaB40D39bd02028545a4FfA7D287cC3E2; + + address public constant SWIFT_V2_PROTOCOL = 0x40fFE85A28DC9993541449464d7529a922142960; + + address public constant SWIFT_V1_PROTOCOL = 0xC38e4e6A15593f908255214653d3D947CA1c2338; + + error InvalidSwiftVersion(uint8 version); + + mapping(bytes32 => mapping(uint256 => uint16)) public caip2ToWormholeChainId; + mapping(Universe => bytes32) public universeToCaip2Namespace; + + /// @notice Emitted when a Wormhole chain mapping is updated + /// @param namespaceHash CAIP-2 namespace hash + /// @param chainId Chain ID within the namespace + /// @param wormholeChainId Corresponding Wormhole chain ID + event WormholeChainMappingSet(bytes32 indexed namespaceHash, uint256 indexed chainId, uint16 wormholeChainId); + + /// @notice Initialize router with default EVM chain mappings + constructor(address _owner) Ownable(_owner) { + bytes32 eip155 = keccak256("eip155"); + caip2ToWormholeChainId[eip155][1] = 2; + caip2ToWormholeChainId[eip155][8453] = 30; + caip2ToWormholeChainId[eip155][42_161] = 23; + caip2ToWormholeChainId[eip155][10] = 24; + caip2ToWormholeChainId[eip155][43_114] = 6; + caip2ToWormholeChainId[eip155][137] = 5; + caip2ToWormholeChainId[eip155][56] = 4; + + // Map Universe enum to CAIP-2 namespaces + universeToCaip2Namespace[Universe.ETHEREUM] = eip155; + } + + /// @notice Process cross-chain token transfer via Mayan Swift V2 or V1 + /// @dev Only supports ETHEREUM universe sources. Destination chain must be configured. + /// @param request Action struct containing source, destination, and recipient details + /// @param data ABI-encoded (SwiftVersion, remaining data) + function processTransfer(Request calldata request, bytes calldata data) external payable override { + require(request.sources.length == request.destinations.length, "Invalid RFF"); + (uint256 chainIndex, bytes memory actualData) = abi.decode(data, (uint256, bytes)); + + _processTransferV2(request, chainIndex, actualData); + } + + /// @notice Process cross-chain transfer via Mayan Swift V2 + /// @param request Action struct containing source, destination, and recipient details + /// @param data ABI-encoded V2 payload + function _processTransferV2(Request calldata request, uint256 chainIndex, bytes memory data) internal { + address tokenIn = address(uint160(uint256(request.sources[chainIndex].contractAddress))); + uint256 amountIn = request.sources[chainIndex].value; + + ( + uint8 tokenOutDecimals, + uint64 gasDrop, + bytes32 referrerAddr, + uint64 cancelFee, + uint64 refundFee, + uint64 deadline, + uint8 referrerBps, + uint8 auctionMode, + bytes32 random, + uint8 payloadType, + address swapProtocol, + bytes memory swapData, + address middleToken, + uint256 minMiddleAmount + ) = abi.decode( + data, + ( + uint8, + uint64, + bytes32, + uint64, + uint64, + uint64, + uint8, + uint8, + bytes32, + uint8, + address, + bytes, + address, + uint256 + ) + ); + + uint16 wormholeChainId = + caip2ToWormholeChainId[universeToCaip2Namespace[request.destinationUniverse]][request.destinationChainID]; + require(wormholeChainId != 0, "Unsupported destination chain"); + + uint256 normalizedMinAmountOut = request.destinations[chainIndex].value; + + if (tokenOutDecimals > 8) { + normalizedMinAmountOut = normalizedMinAmountOut / (10 ** (tokenOutDecimals - 8)); + } + + IMayanSwiftV2.OrderParams memory orderParams = IMayanSwiftV2.OrderParams({ + payloadType: payloadType, + trader: extractAddress(request.parties), + destAddr: request.recipientAddress, + destChainId: wormholeChainId, + referrerAddr: referrerAddr, + tokenOut: request.destinations[chainIndex].contractAddress, + minAmountOut: uint64(normalizedMinAmountOut), + gasDrop: gasDrop, + cancelFee: cancelFee, + refundFee: refundFee, + deadline: deadline, + referrerBps: referrerBps, + auctionMode: auctionMode, + random: random + }); + + if (tokenIn == address(0)) { + bytes memory protocolData = abi.encodeWithSelector( + IMayanSwiftV2.createOrderWithToken.selector, middleToken, minMiddleAmount, orderParams, bytes("") + ); + + IMayanForwarder(MAYAN_FORWARDER).swapAndForwardEth{value: amountIn}( + amountIn, swapProtocol, swapData, middleToken, minMiddleAmount, SWIFT_V2_PROTOCOL, protocolData + ); + } else { + bytes memory protocolData = abi.encodeWithSelector( + IMayanSwiftV2.createOrderWithToken.selector, tokenIn, amountIn, orderParams, bytes("") + ); + IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + IERC20(tokenIn).approve(MAYAN_FORWARDER, amountIn); + + IMayanForwarder.PermitParams memory emptyPermit; + IMayanForwarder(MAYAN_FORWARDER) + .forwardERC20(tokenIn, amountIn, emptyPermit, SWIFT_V2_PROTOCOL, protocolData); + } + } + + /// @notice Set or update CAIP-2 namespace and chain ID to Wormhole chain ID mapping + /// @dev Only callable by contract owner + /// @param namespaceHash CAIP-2 namespace hash (e.g., keccak256("eip155") for EVM) + /// @param chainId Chain ID within the namespace (e.g., 1 for Ethereum, 8453 for Base) + /// @param wormholeChainId Corresponding Wormhole chain ID + function setWormholeChainMapping(bytes32 namespaceHash, uint256 chainId, uint16 wormholeChainId) + external + onlyOwner + { + caip2ToWormholeChainId[namespaceHash][chainId] = wormholeChainId; + emit WormholeChainMappingSet(namespaceHash, chainId, wormholeChainId); + } + + function extractAddress(Party[] memory parties) internal pure returns (bytes32 user) { + for (uint256 i = 0; i < parties.length; ++i) { + if (parties[i].universe == Universe.ETHEREUM) { + return parties[i].address_; + } + } + revert("Vault: Party not found"); + } +} diff --git a/src/types.sol b/src/types.sol new file mode 100644 index 0000000..3850b00 --- /dev/null +++ b/src/types.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +enum Universe { + ETHEREUM, + FUEL, + SOLANA, + TRON +} + +enum RFFState { + UNPROCESSED, + DEPOSITED, + FULFILLED +} + +enum Route { + NATIVE, + MAYAN +} + +struct SourcePair { + Universe universe; + uint256 chainID; + bytes32 contractAddress; + uint256 value; + uint256 fee; +} + +struct DestinationPair { + bytes32 contractAddress; + uint256 value; +} + +struct Party { + Universe universe; + bytes32 address_; // address is a reserved keyword +} + +struct Request { + SourcePair[] sources; + Universe destinationUniverse; + uint256 destinationChainID; + bytes32 recipientAddress; + DestinationPair[] destinations; + uint256 nonce; + uint256 expiry; + Party[] parties; +} + +struct SettleData { + Universe universe; + uint256 chainID; + address vaultAddress; + address[] solvers; + address[] contractAddresses; + uint256[] amounts; + uint256 nonce; +} + diff --git a/test/BaseVaultTest.t.sol b/test/BaseVaultTest.t.sol index be8dc38..e5ee385 100644 --- a/test/BaseVaultTest.t.sol +++ b/test/BaseVaultTest.t.sol @@ -262,7 +262,9 @@ abstract contract BaseVaultTest is Test { uint256[] memory amounts = new uint256[](1); amounts[0] = amount; - return _createSettleData(Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce); + return _createSettleData( + Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce + ); } // Helper Functions - Address Conversion diff --git a/test/ERC20Sweeper.t.sol b/test/ERC20Sweeper.t.sol new file mode 100644 index 0000000..3fb7565 --- /dev/null +++ b/test/ERC20Sweeper.t.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Test} from "forge-std/Test.sol"; +import {ERC20Sweeper} from "../src/ERC20Sweeper.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; + +contract ERC20SweeperTest is Test { + ERC20Sweeper public sweeper; + MockERC20 public token; + + address public user; + address public recipient; + + function setUp() public { + user = makeAddr("user"); + recipient = makeAddr("recipient"); + + sweeper = new ERC20Sweeper(); + token = new MockERC20("Test Token", "TEST"); + + token.mint(user, 1000e18); + } + + function test_SweepERC20_TransfersEntireBalance() public { + vm.prank(user); + token.approve(address(sweeper), type(uint256).max); + + uint256 userBalanceBefore = token.balanceOf(user); + + vm.prank(user); + sweeper.sweepERC20(token, recipient); + + assertEq(token.balanceOf(user), 0); + assertEq(token.balanceOf(recipient), userBalanceBefore); + } + + function test_SweepERC20_DoesNothingOnZeroBalance() public { + address emptyUser = makeAddr("emptyUser"); + + vm.prank(emptyUser); + token.approve(address(sweeper), type(uint256).max); + + vm.prank(emptyUser); + sweeper.sweepERC20(token, recipient); + + assertEq(token.balanceOf(recipient), 0); + } + + function test_SweepERC20_Fuzz(uint256 amount) public { + vm.assume(amount > 0 && amount < type(uint128).max); + + address fuzzUser = makeAddr("fuzzUser"); + token.mint(fuzzUser, amount); + + vm.prank(fuzzUser); + token.approve(address(sweeper), type(uint256).max); + + vm.prank(fuzzUser); + sweeper.sweepERC20(token, recipient); + + assertEq(token.balanceOf(fuzzUser), 0); + assertEq(token.balanceOf(recipient), amount); + } +} diff --git a/test/MayanRouter.t.sol b/test/MayanRouter.t.sol new file mode 100644 index 0000000..ee48cbf --- /dev/null +++ b/test/MayanRouter.t.sol @@ -0,0 +1,733 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; + +import {Test} from "forge-std/Test.sol"; +import {MayanRouter} from "../src/routes/mayan.sol"; +import {Router} from "../src/Router.sol"; +import {Vault} from "../src/Vault.sol"; +import {MockERC20} from "./mocks/MockERC20.sol"; +import {Request, SourcePair, Party, Universe, Route, DestinationPair} from "../src/types.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +contract MayanRouterTest is Test { + MayanRouter public mayanRouter; + Router public router; + Vault public vault; + MockERC20 public token; + + address public admin; + address public user; + uint256 public userPrivateKey; + address public recipient; + + // Mayan Protocol addresses on Base + address constant MAYAN_FORWARDER = 0x337685fdaB40D39bd02028545a4FfA7D287cC3E2; + // Swift V2 protocol address on Base + address constant SWIFT_V2_BASE = 0xc05fb021704D4709c8C058da691fdf4070574685; + // Swift V1 protocol address on Base + address constant SWIFT_V1_BASE = 0xC38e4e6A15593f908255214653d3D947CA1c2338; + + // Real swap parameters from Base tx + address constant SWAP_PROTOCOL = 0x0000000000001fF3684f28c67538d4D072C22734; + address constant MIDDLE_TOKEN = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + uint256 constant MIN_MIDDLE_AMOUNT = 426303507; + bytes constant SWAP_DATA = + hex"2213bc0b0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f2359000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002985b825cff80000000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000009241fff991f000000000000000000000000337685fdab40d39bd02028545a4ffa7d287cc3e2000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000000000000000000000000000000000001984bf3300000000000000000000000000000000000000000000000000000000000000a0fd0aba5d02eb31646adca10d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000007a00000000000000000000000000000000000000000000000000000000000000044bd01c2260000000000000000000000000000000000000000000000000000000069efdb8b00000000000000000000000000000000000000000000000002985b825cff800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010438c9c147000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee00000000000000000000000000000000000000000000000000000000000027100000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000024d0e30db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e48d68a1560000000000000000000000007747f8d2a76bd6345cc29622a946a929647f23590000000000000000000000000000000000000000000000000000000000000ef90000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000404200000000000000000000000000000000000006040000c8fffd8963efd1fc6a506488495d951d5263988d250b3e328455c4059eeb9e3f84b5543f74e24e7e1b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c438c9c1470000000000000000000000004200000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000271000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e43ae8b2980000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000199afe5b9594aa0000000000000000000000000000000000000000000000000000000000fb7365d0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c438c9c1470000000000000000000000000b3e328455c4059eeb9e3f84b5543f74e24e7e1b000000000000000000000000000000000000000000000000000000000000271000000000000000000000000055555522005bcae1c2424d474bfd5ed477749e3e000000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e43ae8b2980000000000000000000000000b3e328455c4059eeb9e3f84b5543f74e24e7e1b000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda0291300000000000000000000000000000000000000000000000cd9363105af0a80000000000000000000000000000000000000000000000000000000000009c5203e0000000000000000000000007747f8d2a76bd6345cc29622a946a929647f235900000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da956157000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000000000019896ad500000000000000000000000000000000000000000000000000000000000027100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; + + function setUp() public { + vm.createSelectFork("base", 45268673); + vm.warp(1777326693); + + admin = makeAddr("admin"); + userPrivateKey = 0xA11CE; + user = vm.addr(userPrivateKey); + recipient = makeAddr("recipient"); + + // Deploy MayanRouter + mayanRouter = new MayanRouter(admin); + + // Deploy Router implementation and proxy + router = new Router(admin); + + // Deploy Vault implementation and proxy + Vault vaultImpl = new Vault(); + bytes memory vaultInitData = abi.encodeWithSelector(Vault.initialize.selector, admin); + ERC1967Proxy vaultProxy = new ERC1967Proxy(address(vaultImpl), vaultInitData); + vault = Vault(payable(address(vaultProxy))); + + // Configure Router with MayanRouter + vm.prank(admin); + router.setRouter(Route.MAYAN, address(mayanRouter)); + + // Configure Vault with Router + vm.prank(admin); + vault.setRouter(address(router)); + + // Deploy mock token for testing + token = new MockERC20("Test Token", "TEST"); + token.mint(user, 1000e18); + + vm.deal(user, 100 ether); + } + + function _signRequest(Request memory request, uint256 privateKey) internal pure returns (bytes memory) { + bytes32 requestHash = keccak256( + abi.encode( + request.sources, + request.destinationUniverse, + request.destinationChainID, + request.recipientAddress, + request.destinations, + request.nonce, + request.expiry, + request.parties + ) + ); + bytes memory msgBytes = + abi.encodePacked("Sign this intent to proceed \n", Strings.toHexString(uint256(requestHash), 32)); + bytes32 signedMessageHash = MessageHashUtils.toEthSignedMessageHash(msgBytes); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, signedMessageHash); + return abi.encodePacked(r, s, v); + } + + function test_ProcessTransfer_ERC20() public { + // Approve router to spend tokens + vm.prank(user); + token.approve(address(mayanRouter), 100e18); + + // Prepare V2 transfer data (14 params - direct V2 payload encoding, no SwiftVersion wrapper) + bytes memory data = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + address(0), // swapProtocol + bytes(""), // swapData + address(0), // middleToken + uint256(0) // minMiddleAmount + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: 8453, + contractAddress: bytes32(uint256(uint160(address(token)))), + value: 100e18, + fee: 0 + }); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(uint256(uint160(address(token)))), value: 90e18}); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + Request memory request = Request({ + sources: sources, + destinationUniverse: Universe.ETHEREUM, + destinationChainID: 1, + recipientAddress: bytes32(uint256(uint160(user))), + destinations: destinations, + nonce: 12_345, + expiry: block.timestamp + 3600, + parties: parties + }); + + // Encode data with chain index + bytes memory encodedData = abi.encode(uint256(0), data); + + uint256 userBalanceBefore = token.balanceOf(user); + vm.prank(user); + mayanRouter.processTransfer(request, encodedData); + uint256 userBalanceAfter = token.balanceOf(user); + + assertEq(userBalanceBefore - 100e18, userBalanceAfter); + } + + uint256 constant SWAP_AMOUNT = 0.187 ether; + + function test_ProcessTransfer_ETH() public { + // Prepare transfer data with real swap params from mainnet tx (direct V2 payload) + bytes memory data = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + SWAP_PROTOCOL, + SWAP_DATA, + MIDDLE_TOKEN, + MIN_MIDDLE_AMOUNT + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, chainID: 8453, contractAddress: bytes32(0), value: SWAP_AMOUNT, fee: 0 + }); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(0), value: 0.5 ether}); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + Request memory request = Request({ + sources: sources, + destinationUniverse: Universe.ETHEREUM, + destinationChainID: 1, + recipientAddress: bytes32(uint256(uint160(user))), + destinations: destinations, + nonce: 12_346, + expiry: block.timestamp + 3600, + parties: parties + }); + + // Encode data with chain index + bytes memory encodedData = abi.encode(uint256(0), data); + + uint256 userBalanceBefore = user.balance; + + // Execute the transfer - should not revert with real swap params + vm.prank(user); + mayanRouter.processTransfer{value: SWAP_AMOUNT}(request, encodedData); + + // User balance should decrease by swap amount (sent to router) + assertEq(user.balance, userBalanceBefore - SWAP_AMOUNT); + } + + function test_VaultDepositRouter_ERC20() public { + // Approve vault to spend tokens + vm.prank(user); + token.approve(address(vault), 100e18); + + // Prepare route data (direct V2 payload - 14 params, no SwiftVersion wrapper) + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + address(0), // swapProtocol + bytes(""), // swapData + address(0), // middleToken + uint256(0) // minMiddleAmount + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: block.chainid, + contractAddress: bytes32(uint256(uint160(address(token)))), + value: 100e18, + fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(uint256(uint160(address(token)))), value: 90e18}); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 1001, + expiry: uint64(block.timestamp + 3600) + }); + + bytes memory signature = _signRequest(request, userPrivateKey); + + uint256 userBalanceBefore = token.balanceOf(user); + uint256 vaultBalanceBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.depositRouter(request, signature, 0, Route.MAYAN, routeData); + + uint256 userBalanceAfter = token.balanceOf(user); + uint256 vaultBalanceAfter = token.balanceOf(address(vault)); + + // User should have transferred tokens to vault + assertEq(userBalanceBefore - 100e18, userBalanceAfter); + // Vault should not hold tokens (forwarded to router/mayan) + assertEq(vaultBalanceBefore, vaultBalanceAfter); + } + + function test_VaultDepositRouter_ETH() public { + // Prepare route data with real swap params (direct V2 payload) + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + SWAP_PROTOCOL, + SWAP_DATA, + MIDDLE_TOKEN, + MIN_MIDDLE_AMOUNT + ); + + // Create Request object for ETH transfer + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: block.chainid, + contractAddress: bytes32(0), // Native ETH + value: SWAP_AMOUNT, + fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({ + contractAddress: bytes32(0), // ETH + value: 0.5 ether + }); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 1002, + expiry: uint64(block.timestamp + 3600) + }); + + bytes memory signature = _signRequest(request, userPrivateKey); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + // Execute the transfer - should not revert with real swap params + vm.prank(user); + vault.depositRouter{value: SWAP_AMOUNT}(request, signature, 0, Route.MAYAN, routeData); + + // User balance should decrease by swap amount + assertEq(user.balance, userBalanceBefore - SWAP_AMOUNT); + // Vault should not hold ETH (forwarded to router/mayan) + assertEq(address(vault).balance, vaultBalanceBefore); + } + + function test_VaultDepositRouter_RevertInvalidSignature() public { + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, chainID: block.chainid, contractAddress: bytes32(0), value: 1 ether, fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(0), value: 0.5 ether}); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 1003, + expiry: uint64(block.timestamp + 3600) + }); + + // Sign with wrong private key + uint256 wrongPrivateKey = 0xBAD; + bytes memory wrongSignature = _signRequest(request, wrongPrivateKey); + + // Prepare route data (direct V2 payload - 14 params) + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), + bytes32(0), + uint64(0), + uint64(0), + uint64(block.timestamp + 3600), + uint8(0), + uint8(0), + bytes32(0), + uint8(0), + address(0), + bytes(""), + address(0), + uint256(0) + ); + + vm.prank(user); + vm.expectRevert("Vault: Invalid signature or from"); + vault.depositRouter{value: 1 ether}(request, wrongSignature, 0, Route.MAYAN, routeData); + } + + function test_VaultDepositRouter_RevertNonceReuse() public { + vm.prank(user); + token.approve(address(vault), 200e18); + + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: block.chainid, + contractAddress: bytes32(uint256(uint160(address(token)))), + value: 100e18, + fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(uint256(uint160(address(token)))), value: 90e18}); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 2001, + expiry: uint64(block.timestamp + 3600) + }); + + bytes memory signature = _signRequest(request, userPrivateKey); + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), + bytes32(0), + uint64(0), + uint64(0), + uint64(block.timestamp + 3600), + uint8(0), + uint8(0), + bytes32(0), + uint8(0), + address(0), + bytes(""), + address(0), + uint256(0) + ); + + // First deposit should succeed + vm.prank(user); + vault.depositRouter(request, signature, 0, Route.MAYAN, routeData); + + // Second deposit with same nonce should revert + vm.prank(user); + vm.expectRevert("Vault: Nonce already used"); + vault.depositRouter(request, signature, 0, Route.MAYAN, routeData); + } + + function test_ProcessTransferV2_ERC20() public { + // Approve router to spend tokens + vm.prank(user); + token.approve(address(mayanRouter), 100e18); + + // Encode V2 data (direct V2 payload encoding - 14 params, no SwiftVersion wrapper) + bytes memory data = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + address(0), // swapProtocol + bytes(""), // swapData + address(0), // middleToken + uint256(0) // minMiddleAmount + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: 8453, + contractAddress: bytes32(uint256(uint160(address(token)))), + value: 100e18, + fee: 0 + }); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(uint256(uint160(address(token)))), value: 90e18}); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + Request memory request = Request({ + sources: sources, + destinationUniverse: Universe.ETHEREUM, + destinationChainID: 1, + recipientAddress: bytes32(uint256(uint160(user))), + destinations: destinations, + nonce: 12_347, + expiry: block.timestamp + 3600, + parties: parties + }); + + // Encode data with chain index + bytes memory encodedData = abi.encode(uint256(0), data); + + uint256 userBalanceBefore = token.balanceOf(user); + vm.prank(user); + mayanRouter.processTransfer(request, encodedData); + uint256 userBalanceAfter = token.balanceOf(user); + + assertEq(userBalanceBefore - 100e18, userBalanceAfter); + } + + function test_ProcessTransferV2_ETH() public { + // Encode V2 data with real swap params from mainnet tx + bytes memory data = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + SWAP_PROTOCOL, + SWAP_DATA, + MIDDLE_TOKEN, + MIN_MIDDLE_AMOUNT + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, chainID: 8453, contractAddress: bytes32(0), value: SWAP_AMOUNT, fee: 0 + }); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(0), value: 0.5 ether}); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + Request memory request = Request({ + sources: sources, + destinationUniverse: Universe.ETHEREUM, + destinationChainID: 1, + recipientAddress: bytes32(uint256(uint160(user))), + destinations: destinations, + nonce: 12_348, + expiry: block.timestamp + 3600, + parties: parties + }); + + // Encode data with chain index + bytes memory encodedData = abi.encode(uint256(0), data); + + uint256 userBalanceBefore = user.balance; + + // Execute the transfer + vm.prank(user); + mayanRouter.processTransfer{value: SWAP_AMOUNT}(request, encodedData); + + // User balance should decrease by swap amount (sent to router) + assertEq(user.balance, userBalanceBefore - SWAP_AMOUNT); + } + + function test_ProcessTransfer_InvalidVersion() public { + // Create invalid data that will fail V2 decode + bytes memory invalidData = abi.encode(uint8(2), bytes("")); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, chainID: 8453, contractAddress: bytes32(0), value: 1 ether, fee: 0 + }); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(0), value: 0.5 ether}); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + Request memory request = Request({ + sources: sources, + destinationUniverse: Universe.ETHEREUM, + destinationChainID: 1, + recipientAddress: bytes32(uint256(uint160(user))), + destinations: destinations, + nonce: 12_349, + expiry: block.timestamp + 3600, + parties: parties + }); + + // Encode data with chain index + bytes memory encodedData = abi.encode(uint256(0), invalidData); + + vm.prank(user); + vm.expectRevert(); + mayanRouter.processTransfer{value: 1 ether}(request, encodedData); + } + + function test_VaultDepositRouter_V2_ERC20() public { + // Approve vault to spend tokens + vm.prank(user); + token.approve(address(vault), 100e18); + + // Encode V2 data (direct V2 payload - 14 params, no SwiftVersion wrapper) + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + address(0), // swapProtocol + bytes(""), // swapData + address(0), // middleToken + uint256(0) // minMiddleAmount + ); + + // Create Request object + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: block.chainid, + contractAddress: bytes32(uint256(uint160(address(token)))), + value: 100e18, + fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({contractAddress: bytes32(uint256(uint160(address(token)))), value: 90e18}); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 1004, + expiry: uint64(block.timestamp + 3600) + }); + + bytes memory signature = _signRequest(request, userPrivateKey); + + uint256 userBalanceBefore = token.balanceOf(user); + uint256 vaultBalanceBefore = token.balanceOf(address(vault)); + + vm.prank(user); + vault.depositRouter(request, signature, 0, Route.MAYAN, routeData); + + uint256 userBalanceAfter = token.balanceOf(user); + uint256 vaultBalanceAfter = token.balanceOf(address(vault)); + + // User should have transferred tokens to vault + assertEq(userBalanceBefore - 100e18, userBalanceAfter); + // Vault should not hold tokens (forwarded to router/mayan) + assertEq(vaultBalanceBefore, vaultBalanceAfter); + } + + function test_VaultDepositRouter_V2_ETH() public { + // Encode V2 data with real swap params from mainnet tx + bytes memory routeData = abi.encode( + uint8(0), // tokenOutDecimals + uint64(0), // gasDrop + bytes32(0), // referrerAddr + uint64(0), // cancelFee + uint64(0), // refundFee + uint64(block.timestamp + 3600), // deadline + uint8(0), // referrerBps + uint8(0), // auctionMode + bytes32(0), // random + uint8(0), // payloadType + SWAP_PROTOCOL, + SWAP_DATA, + MIDDLE_TOKEN, + MIN_MIDDLE_AMOUNT + ); + + // Create Request object for ETH transfer + SourcePair[] memory sources = new SourcePair[](1); + sources[0] = SourcePair({ + universe: Universe.ETHEREUM, + chainID: block.chainid, + contractAddress: bytes32(0), // Native ETH + value: SWAP_AMOUNT, + fee: 0 + }); + + Party[] memory parties = new Party[](1); + parties[0] = Party({universe: Universe.ETHEREUM, address_: bytes32(uint256(uint160(user)))}); + + DestinationPair[] memory destinations = new DestinationPair[](1); + destinations[0] = DestinationPair({ + contractAddress: bytes32(0), // ETH + value: 0.5 ether + }); + + Request memory request = Request({ + sources: sources, + recipientAddress: bytes32(uint256(uint160(recipient))), + parties: parties, + destinationUniverse: Universe.ETHEREUM, + destinations: destinations, + destinationChainID: 1, + nonce: 1005, + expiry: uint64(block.timestamp + 3600) + }); + + bytes memory signature = _signRequest(request, userPrivateKey); + + uint256 userBalanceBefore = user.balance; + uint256 vaultBalanceBefore = address(vault).balance; + + // Execute the transfer + vm.prank(user); + vault.depositRouter{value: SWAP_AMOUNT}(request, signature, 0, Route.MAYAN, routeData); + + // User balance should decrease by swap amount + assertEq(user.balance, userBalanceBefore - SWAP_AMOUNT); + // Vault should not hold ETH (forwarded to router/mayan) + assertEq(address(vault).balance, vaultBalanceBefore); + } +} diff --git a/test/Vault.fuzz.t.sol b/test/Vault.fuzz.t.sol index e9ba0fb..dcee586 100644 --- a/test/Vault.fuzz.t.sol +++ b/test/Vault.fuzz.t.sol @@ -447,8 +447,9 @@ contract VaultFuzzTest is BaseVaultTest { amounts[0] = amount1; amounts[1] = amount2; - Vault.SettleData memory settleData = - _createSettleData(Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce); + Vault.SettleData memory settleData = _createSettleData( + Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce + ); // Sign with verifier key bytes32 structHash = keccak256( diff --git a/test/Vault.t.sol b/test/Vault.t.sol new file mode 100644 index 0000000..b4bc572 --- /dev/null +++ b/test/Vault.t.sol @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.29; +// +// import {Test} from "forge-std/Test.sol"; +// import {Vault} from "../src/Vault.sol"; +// import {Request, SourcePair, DestinationPair} from "../src/types.sol"; +// import {MockERC20} from "./mocks/MockERC20.sol"; +// import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +// +// contract VaultTest is Test { +// Vault public vault; +// Vault public vaultImpl; +// MockERC20 public usdc; +// +// address public owner; +// address public user; +// address public solver; +// address public settlementVerifier; +// +// uint256 public ownerPk; +// uint256 public userPk; +// uint256 public solverPk; +// uint256 public settlementVerifierPk; +// +// bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); +// bytes32 public constant SETTLEMENT_VERIFIER_ROLE = +// keccak256("SETTLEMENT_VERIFIER_ROLE"); +// +// function setUp() public { +// ownerPk = 0x1; +// userPk = 0x2; +// solverPk = 0x3; +// settlementVerifierPk = 0x4; +// +// owner = vm.addr(ownerPk); +// user = vm.addr(userPk); +// solver = vm.addr(solverPk); +// settlementVerifier = vm.addr(settlementVerifierPk); +// +// usdc = new MockERC20("USD Coin", "USDC", 6); +// +// vaultImpl = new Vault(); +// bytes memory initData = abi.encodeCall(Vault.initialize, (owner)); +// ERC1967Proxy proxy = new ERC1967Proxy(address(vaultImpl), initData); +// vault = Vault(payable(address(proxy))); +// +// vm.prank(owner); +// vault.grantRole(SETTLEMENT_VERIFIER_ROLE, settlementVerifier); +// +// vm.deal(user, 100 ether); +// vm.deal(solver, 100 ether); +// vm.deal(address(vault), 1 ether); +// } +// +// function _addressToBytes32(address addr) internal pure returns (bytes32) { +// return bytes32(uint256(uint160(addr))); +// } +// +// function _createRequest( +// address sourceToken, +// uint256 sourceAmount, +// address destToken, +// uint256 destAmount, +// uint256 destChainId, +// uint256 nonce, +// uint256 expiry, +// address userAddr +// ) internal view returns (Request memory) { +// SourcePair[] memory sources = new SourcePair[](1); +// sources[0] = SourcePair({ +// universe: Vault.Universe.ETHEREUM, +// chainID: block.chainid, +// contractAddress: _addressToBytes32(sourceToken), +// value: sourceAmount +// }); +// +// DestinationPair[] memory destinations = new DestinationPair[](1); +// destinations[0] = Vault.DestinationPair({ +// contractAddress: _addressToBytes32(destToken), +// value: destAmount +// }); +// +// Party[] memory parties = new Party[](1); +// parties[0] = Party({ +// universe: Universe.ETHEREUM, +// address_: _addressToBytes32(userAddr) +// }); +// +// return +// Request({ +// sources: sources, +// destinationUniverse: Vault.Universe.ETHEREUM, +// destinationChainID: destChainId, +// recipientAddress: _addressToBytes32(userAddr), +// destinations: destinations, +// nonce: nonce, +// expiry: expiry, +// parties: parties +// }); +// } +// +// function _hashRequest( +// Request memory request +// ) internal pure returns (bytes32) { +// return +// keccak256( +// abi.encode( +// request.sources, +// request.destinationUniverse, +// request.destinationChainID, +// request.recipientAddress, +// request.destinations, +// request.nonce, +// request.expiry, +// request.parties +// ) +// ); +// } +// +// function _signRequest( +// Request memory request, +// uint256 pk +// ) internal pure returns (bytes memory) { +// bytes32 requestHash = _hashRequest(request); +// bytes32 ethSignedHash = keccak256( +// abi.encodePacked("\x19Ethereum Signed Message:\n32", requestHash) +// ); +// (uint8 v, bytes32 r, bytes32 s) = vm.sign(pk, ethSignedHash); +// return abi.encodePacked(r, s, v); +// } +// +// function test_Initialize_AssignsAdminRole() public view { +// assertTrue(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), owner)); +// } +// +// function test_Initialize_AssignsUpgraderRole() public view { +// assertTrue(vault.hasRole(UPGRADER_ROLE, owner)); +// } +// +// function test_NonAdmin_DoesNotHaveAdminRole() public view { +// assertFalse(vault.hasRole(vault.DEFAULT_ADMIN_ROLE(), user)); +// } +// +// function test_Deposit_ERC20Tokens() public { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// usdc.mint(user, amount); +// +// vm.prank(user); +// usdc.approve(address(vault), amount); +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// 2, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// vm.expectEmit(true, false, false, true); +// emit Vault.Deposit( +// keccak256( +// abi.encodePacked( +// "\x19Ethereum Signed Message:\n32", +// _hashRequest(request) +// ) +// ), +// user +// ); +// +// vault.deposit(request, signature, 0); +// +// assertEq(usdc.balanceOf(user), 0); +// assertEq(usdc.balanceOf(address(vault)), amount); +// } +// +// function test_Deposit_NativeTokens() public { +// uint256 amount = 1 ether; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// Request memory request = _createRequest( +// address(0), +// amount, +// address(0), +// amount, +// 2, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// uint256 vaultBalanceBefore = address(vault).balance; +// +// vault.deposit{value: amount}(request, signature, 0); +// +// assertEq(address(vault).balance, vaultBalanceBefore + amount); +// } +// +// function test_Deposit_RevertsOnDuplicateNonce() public { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// usdc.mint(user, amount * 2); +// +// vm.prank(user); +// usdc.approve(address(vault), amount * 2); +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// 2, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// vault.deposit(request, signature, 0); +// +// vm.expectRevert("Vault: Nonce already used"); +// vault.deposit(request, signature, 0); +// } +// +// function test_Deposit_RevertsOnExpiredRequest() public { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp - 1; +// +// usdc.mint(user, amount); +// +// vm.prank(user); +// usdc.approve(address(vault), amount); +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// 2, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// vm.expectRevert("Vault: Request expired"); +// vault.deposit(request, signature, 0); +// } +// +// function test_Fulfil_TransfersTokensToRecipient() public { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// usdc.mint(solver, amount); +// +// vm.prank(solver); +// usdc.approve(address(vault), amount); +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// block.chainid, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// vm.prank(solver); +// vault.fulfil(request, signature); +// +// assertEq(usdc.balanceOf(user), amount); +// } +// +// function test_Fulfil_NativeTokens() public { +// uint256 amount = 1 ether; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// Request memory request = _createRequest( +// address(0), +// amount, +// address(0), +// amount, +// block.chainid, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// uint256 userBalanceBefore = user.balance; +// +// vm.prank(solver); +// vault.fulfil{value: amount}(request, signature); +// +// assertEq(user.balance, userBalanceBefore + amount); +// } +// +// function test_Fulfil_RevertsOnDuplicateNonce() public { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// usdc.mint(solver, amount * 2); +// +// vm.prank(solver); +// usdc.approve(address(vault), amount * 2); +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// block.chainid, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// vm.prank(solver); +// vault.fulfil(request, signature); +// +// vm.prank(solver); +// vm.expectRevert("Vault: Nonce already used"); +// vault.fulfil(request, signature); +// } +// +// function test_Settle_TransfersToSolvers() public { +// uint256 erc20Amount = 1000e6; +// uint256 nativeAmount = 1 ether; +// +// usdc.mint(address(vault), erc20Amount); +// +// address[] memory solvers_ = new address[](2); +// solvers_[0] = solver; +// solvers_[1] = solver; +// +// address[] memory tokens = new address[](2); +// tokens[0] = address(usdc); +// tokens[1] = address(0); +// +// uint256[] memory amounts = new uint256[](2); +// amounts[0] = erc20Amount; +// amounts[1] = nativeAmount; +// +// uint256 nonce = 1; +// +// Vault.SettleData memory settleData = Vault.SettleData({ +// universe: Vault.Universe.ETHEREUM, +// chainID: block.chainid, +// solvers: solvers_, +// contractAddresses: tokens, +// amounts: amounts, +// nonce: nonce +// }); +// +// bytes32 structHash = keccak256( +// abi.encode( +// settleData.universe, +// settleData.chainID, +// settleData.solvers, +// settleData.contractAddresses, +// settleData.amounts, +// settleData.nonce +// ) +// ); +// bytes32 ethSignedHash = keccak256( +// abi.encodePacked("\x19Ethereum Signed Message:\n32", structHash) +// ); +// (uint8 v, bytes32 r, bytes32 s) = vm.sign( +// settlementVerifierPk, +// ethSignedHash +// ); +// bytes memory signature = abi.encodePacked(r, s, v); +// +// uint256 solverUsdcBefore = usdc.balanceOf(solver); +// uint256 solverEthBefore = solver.balance; +// +// vault.settle(settleData, signature); +// +// assertEq(usdc.balanceOf(solver), solverUsdcBefore + erc20Amount); +// assertEq(solver.balance, solverEthBefore + nativeAmount); +// } +// +// function test_Settle_RevertsOnDuplicateNonce() public { +// uint256 amount = 1000e6; +// +// usdc.mint(address(vault), amount * 2); +// +// address[] memory solvers_ = new address[](1); +// solvers_[0] = solver; +// +// address[] memory tokens = new address[](1); +// tokens[0] = address(usdc); +// +// uint256[] memory amounts = new uint256[](1); +// amounts[0] = amount; +// +// uint256 nonce = 1; +// +// Vault.SettleData memory settleData = Vault.SettleData({ +// universe: Vault.Universe.ETHEREUM, +// chainID: block.chainid, +// solvers: solvers_, +// contractAddresses: tokens, +// amounts: amounts, +// nonce: nonce +// }); +// +// bytes32 structHash = keccak256( +// abi.encode( +// settleData.universe, +// settleData.chainID, +// settleData.solvers, +// settleData.contractAddresses, +// settleData.amounts, +// settleData.nonce +// ) +// ); +// bytes32 ethSignedHash = keccak256( +// abi.encodePacked("\x19Ethereum Signed Message:\n32", structHash) +// ); +// (uint8 v, bytes32 r, bytes32 s) = vm.sign( +// settlementVerifierPk, +// ethSignedHash +// ); +// bytes memory signature = abi.encodePacked(r, s, v); +// +// vault.settle(settleData, signature); +// +// vm.expectRevert("Vault: Nonce already used"); +// vault.settle(settleData, signature); +// } +// +// function test_Settle_RevertsWithInvalidSigner() public { +// uint256 amount = 1000e6; +// +// usdc.mint(address(vault), amount); +// +// address[] memory solvers_ = new address[](1); +// solvers_[0] = solver; +// +// address[] memory tokens = new address[](1); +// tokens[0] = address(usdc); +// +// uint256[] memory amounts = new uint256[](1); +// amounts[0] = amount; +// +// uint256 nonce = 1; +// +// Vault.SettleData memory settleData = Vault.SettleData({ +// universe: Vault.Universe.ETHEREUM, +// chainID: block.chainid, +// solvers: solvers_, +// contractAddresses: tokens, +// amounts: amounts, +// nonce: nonce +// }); +// +// bytes32 structHash = keccak256( +// abi.encode( +// settleData.universe, +// settleData.chainID, +// settleData.solvers, +// settleData.contractAddresses, +// settleData.amounts, +// settleData.nonce +// ) +// ); +// bytes32 ethSignedHash = keccak256( +// abi.encodePacked("\x19Ethereum Signed Message:\n32", structHash) +// ); +// (uint8 v, bytes32 r, bytes32 s) = vm.sign(userPk, ethSignedHash); +// bytes memory signature = abi.encodePacked(r, s, v); +// +// vm.expectRevert("Vault: Invalid signature"); +// vault.settle(settleData, signature); +// } +// +// function test_VerifyRequestSignature() public view { +// uint256 amount = 1000e6; +// uint256 nonce = 1; +// uint256 expiry = block.timestamp + 1 hours; +// +// Request memory request = _createRequest( +// address(usdc), +// amount, +// address(usdc), +// amount, +// 2, +// nonce, +// expiry, +// user +// ); +// +// bytes memory signature = _signRequest(request, userPk); +// +// (bool valid, bytes32 hash) = vault.verifyRequestSignature( +// request, +// signature +// ); +// +// assertTrue(valid); +// assertEq( +// hash, +// keccak256( +// abi.encodePacked( +// "\x19Ethereum Signed Message:\n32", +// _hashRequest(request) +// ) +// ) +// ); +// } +// +// receive() external payable {} +// } diff --git a/test/mocks/MockFeeOnTransfer.t.sol b/test/mocks/MockFeeOnTransfer.t.sol index c05c217..9eaea97 100644 --- a/test/mocks/MockFeeOnTransfer.t.sol +++ b/test/mocks/MockFeeOnTransfer.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.29; -import {Test, console2} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; import {MockFeeOnTransfer} from "test/mocks/MockFeeOnTransfer.sol"; contract MockFeeOnTransferTest is Test { diff --git a/test/unit/Vault/AccessControl.t.sol b/test/unit/Vault/AccessControl.t.sol index c930ba8..91b8515 100644 --- a/test/unit/Vault/AccessControl.t.sol +++ b/test/unit/Vault/AccessControl.t.sol @@ -184,8 +184,9 @@ contract AccessControlTest is BaseVaultTest { amounts[0] = 0.5 ether; amounts[1] = 0.5 ether; - Vault.SettleData memory settleData = - _createSettleData(Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce); + Vault.SettleData memory settleData = _createSettleData( + Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce + ); bytes32 structHash = keccak256( abi.encode( @@ -223,8 +224,9 @@ contract AccessControlTest is BaseVaultTest { uint256[] memory amounts = new uint256[](1); // Mismatched length amounts[0] = 0.5 ether; - Vault.SettleData memory settleData = - _createSettleData(Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce); + Vault.SettleData memory settleData = _createSettleData( + Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce + ); bytes32 structHash = keccak256( abi.encode( diff --git a/test/unit/Vault/VaultCore.t.sol b/test/unit/Vault/VaultCore.t.sol index 1f6fe0a..20efa3a 100644 --- a/test/unit/Vault/VaultCore.t.sol +++ b/test/unit/Vault/VaultCore.t.sol @@ -6,7 +6,6 @@ import {BaseVaultTest} from "../../BaseVaultTest.t.sol"; import {SignatureHelper} from "../../helpers/SignatureHelper.sol"; import {Vault} from "../../../contracts/Vault.sol"; import {MockFeeOnTransfer} from "../../mocks/MockFeeOnTransfer.sol"; -import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; // VaultCoreTest - Core Functionality Tests for Vault.sol // @title VaultCoreTest @@ -875,8 +874,9 @@ contract VaultCoreTest is BaseVaultTest { amounts[0] = 0.5 ether; amounts[1] = 1000 * 10 ** 18; - Vault.SettleData memory settleData = - _createSettleData(Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce); + Vault.SettleData memory settleData = _createSettleData( + Vault.Universe.ETHEREUM, block.chainid, address(vault), solvers, contractAddresses, amounts, nonce + ); // Sign with verifier key bytes32 structHash = keccak256( @@ -1194,14 +1194,7 @@ contract VaultCoreTest is BaseVaultTest { address requester = _getAddress(USER_PRIVATE_KEY); Vault.Request memory request = _createRequestForUser( - bytes32(0), - depositAmount, - bytes32(0), - depositAmount, - USER_PRIVATE_KEY, - solver, - nonce, - expiry + bytes32(0), depositAmount, bytes32(0), depositAmount, USER_PRIVATE_KEY, solver, nonce, expiry ); bytes memory signature = sigHelper.signRequest(request, USER_PRIVATE_KEY);