diff --git a/contracts/bridge/adapters/non-evm/SolanaAdapter.sol b/contracts/bridge/adapters/non-evm/SolanaAdapter.sol
new file mode 100644
index 0000000..87f1c34
--- /dev/null
+++ b/contracts/bridge/adapters/non-evm/SolanaAdapter.sol
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/access/AccessControl.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "../../interfaces/IChainAdapter.sol";
+
+/**
+ * @title SolanaAdapter
+ * @notice Bridge adapter for Solana mainnet-beta using an off-chain relay/oracle path
+ * @dev Tracks idempotent fulfillment ids so the relay can resume safely after restarts.
+ */
+contract SolanaAdapter is IChainAdapter, AccessControl, ReentrancyGuard {
+ using SafeERC20 for IERC20;
+
+ bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE");
+ bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
+
+ bool public isActive;
+ uint256 public minFinalitySlots;
+
+ mapping(bytes32 => BridgeRequest) public bridgeRequests;
+ mapping(bytes32 => string) public solanaTxSignatures;
+ mapping(bytes32 => uint256) public confirmedSlots;
+ mapping(bytes32 => bytes32) public fulfillmentIdsByRequest;
+ mapping(bytes32 => bool) public usedFulfillmentIds;
+ mapping(address => uint256) public nonces;
+
+ event SolanaBridgeInitiated(
+ bytes32 indexed requestId,
+ address indexed sender,
+ address indexed token,
+ uint256 amount,
+ string destination,
+ bytes recipient
+ );
+
+ event SolanaBridgeConfirmed(
+ bytes32 indexed requestId,
+ string txSignature,
+ uint256 finalizedSlot,
+ bytes32 indexed fulfillmentId
+ );
+
+ event SolanaBridgeMarkedFailed(bytes32 indexed requestId, string reason);
+ event SolanaBridgeRecovered(bytes32 indexed requestId, BridgeStatus newStatus, string note);
+
+ constructor(address admin) {
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ _grantRole(BRIDGE_OPERATOR_ROLE, admin);
+ _grantRole(ORACLE_ROLE, admin);
+ isActive = true;
+ minFinalitySlots = 32;
+ }
+
+ function getChainType() external pure override returns (string memory) {
+ return "Solana";
+ }
+
+ function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) {
+ return (0, "Solana-Mainnet");
+ }
+
+ function validateDestination(bytes calldata destination) external pure override returns (bool) {
+ uint256 length = destination.length;
+ return length >= 32 && length <= 44;
+ }
+
+ function bridge(
+ address token,
+ uint256 amount,
+ bytes calldata destination,
+ bytes calldata recipient
+ ) external payable override nonReentrant returns (bytes32 requestId) {
+ require(isActive, "Adapter inactive");
+ require(amount > 0, "Zero amount");
+ require(this.validateDestination(destination), "Invalid Solana destination");
+
+ requestId = keccak256(
+ abi.encodePacked(
+ msg.sender,
+ token,
+ amount,
+ destination,
+ recipient,
+ nonces[msg.sender]++,
+ block.timestamp
+ )
+ );
+
+ if (token == address(0)) {
+ require(msg.value >= amount, "Insufficient ETH");
+ } else {
+ IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+ }
+
+ bridgeRequests[requestId] = BridgeRequest({
+ sender: msg.sender,
+ token: token,
+ amount: amount,
+ destinationData: destination,
+ requestId: requestId,
+ status: BridgeStatus.Locked,
+ createdAt: block.timestamp,
+ completedAt: 0
+ });
+
+ emit SolanaBridgeInitiated(
+ requestId,
+ msg.sender,
+ token,
+ amount,
+ string(destination),
+ recipient
+ );
+
+ return requestId;
+ }
+
+ function confirmTransaction(
+ bytes32 requestId,
+ string calldata txSignature,
+ uint256 finalizedSlot,
+ bytes32 fulfillmentId
+ ) external onlyRole(ORACLE_ROLE) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Locked, "Invalid status");
+ require(bytes(txSignature).length > 0, "Missing Solana signature");
+ require(finalizedSlot >= minFinalitySlots, "Insufficient finality");
+ require(fulfillmentId != bytes32(0), "Missing fulfillment id");
+ require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used");
+
+ usedFulfillmentIds[fulfillmentId] = true;
+ fulfillmentIdsByRequest[requestId] = fulfillmentId;
+ solanaTxSignatures[requestId] = txSignature;
+ confirmedSlots[requestId] = finalizedSlot;
+ request.status = BridgeStatus.Confirmed;
+ request.completedAt = block.timestamp;
+
+ emit SolanaBridgeConfirmed(requestId, txSignature, finalizedSlot, fulfillmentId);
+ }
+
+ function markFailed(bytes32 requestId, string calldata reason) external onlyRole(ORACLE_ROLE) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Locked, "Invalid status");
+
+ request.status = BridgeStatus.Failed;
+ request.completedAt = block.timestamp;
+
+ emit SolanaBridgeMarkedFailed(requestId, reason);
+ }
+
+ function recoverBridge(
+ bytes32 requestId,
+ BridgeStatus newStatus,
+ string calldata note
+ ) external onlyRole(BRIDGE_OPERATOR_ROLE) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.requestId != bytes32(0), "Unknown request");
+ require(
+ request.status == BridgeStatus.Failed || request.status == BridgeStatus.Confirmed,
+ "Recovery not allowed"
+ );
+
+ request.status = newStatus;
+ request.completedAt = block.timestamp;
+
+ emit SolanaBridgeRecovered(requestId, newStatus, note);
+ }
+
+ function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) {
+ return bridgeRequests[requestId];
+ }
+
+ function cancelBridge(bytes32 requestId) external override nonReentrant returns (bool) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked, "Cannot cancel");
+ require(msg.sender == request.sender, "Not request sender");
+
+ if (request.token == address(0)) {
+ payable(request.sender).transfer(request.amount);
+ } else {
+ IERC20(request.token).safeTransfer(request.sender, request.amount);
+ }
+
+ request.status = BridgeStatus.Cancelled;
+ request.completedAt = block.timestamp;
+ return true;
+ }
+
+ function estimateFee(
+ address,
+ uint256,
+ bytes calldata
+ ) external pure override returns (uint256 fee) {
+ return 1000000000000000;
+ }
+
+ function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ isActive = _isActive;
+ }
+
+ function setMinFinalitySlots(uint256 slots) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ require(slots > 0, "Zero finality");
+ minFinalitySlots = slots;
+ }
+}
diff --git a/contracts/bridge/adapters/non-evm/TronAdapter.sol b/contracts/bridge/adapters/non-evm/TronAdapter.sol
new file mode 100644
index 0000000..eefb2d9
--- /dev/null
+++ b/contracts/bridge/adapters/non-evm/TronAdapter.sol
@@ -0,0 +1,181 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/access/AccessControl.sol";
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
+import "../../interfaces/IChainAdapter.sol";
+
+/**
+ * @title TronAdapter
+ * @notice Bridge adapter for Tron using an off-chain relay/oracle path
+ * @dev Keeps fulfillment ids to make relay retries idempotent.
+ */
+contract TronAdapter is IChainAdapter, AccessControl, ReentrancyGuard {
+ using SafeERC20 for IERC20;
+
+ bytes32 public constant BRIDGE_OPERATOR_ROLE = keccak256("BRIDGE_OPERATOR_ROLE");
+ bytes32 public constant ORACLE_ROLE = keccak256("ORACLE_ROLE");
+
+ bool public isActive;
+ uint256 public minFinalityBlocks;
+
+ mapping(bytes32 => BridgeRequest) public bridgeRequests;
+ mapping(bytes32 => string) public txHashes;
+ mapping(bytes32 => uint256) public finalizedBlocks;
+ mapping(bytes32 => bytes32) public fulfillmentIdsByRequest;
+ mapping(bytes32 => bool) public usedFulfillmentIds;
+ mapping(address => uint256) public nonces;
+
+ event TronBridgeInitiated(
+ bytes32 indexed requestId,
+ address indexed sender,
+ address indexed token,
+ uint256 amount,
+ string destination
+ );
+
+ event TronBridgeConfirmed(
+ bytes32 indexed requestId,
+ string txHash,
+ uint256 finalizedBlock,
+ bytes32 indexed fulfillmentId
+ );
+
+ event TronBridgeMarkedFailed(bytes32 indexed requestId, string reason);
+
+ constructor(address admin) {
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ _grantRole(BRIDGE_OPERATOR_ROLE, admin);
+ _grantRole(ORACLE_ROLE, admin);
+ isActive = true;
+ minFinalityBlocks = 20;
+ }
+
+ function getChainType() external pure override returns (string memory) {
+ return "Tron";
+ }
+
+ function getChainIdentifier() external pure override returns (uint256 chainId, string memory identifier) {
+ return (0, "Tron-Mainnet");
+ }
+
+ function validateDestination(bytes calldata destination) external pure override returns (bool) {
+ uint256 length = destination.length;
+ return length >= 34 && length <= 36;
+ }
+
+ function bridge(
+ address token,
+ uint256 amount,
+ bytes calldata destination,
+ bytes calldata recipient
+ ) external payable override nonReentrant returns (bytes32 requestId) {
+ require(isActive, "Adapter inactive");
+ require(amount > 0, "Zero amount");
+ require(this.validateDestination(destination), "Invalid Tron destination");
+
+ requestId = keccak256(
+ abi.encodePacked(
+ msg.sender,
+ token,
+ amount,
+ destination,
+ recipient,
+ nonces[msg.sender]++,
+ block.timestamp
+ )
+ );
+
+ if (token == address(0)) {
+ require(msg.value >= amount, "Insufficient ETH");
+ } else {
+ IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
+ }
+
+ bridgeRequests[requestId] = BridgeRequest({
+ sender: msg.sender,
+ token: token,
+ amount: amount,
+ destinationData: destination,
+ requestId: requestId,
+ status: BridgeStatus.Locked,
+ createdAt: block.timestamp,
+ completedAt: 0
+ });
+
+ emit TronBridgeInitiated(requestId, msg.sender, token, amount, string(destination));
+ return requestId;
+ }
+
+ function confirmTransaction(
+ bytes32 requestId,
+ string calldata txHash,
+ uint256 finalizedBlock,
+ bytes32 fulfillmentId
+ ) external onlyRole(ORACLE_ROLE) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Locked, "Invalid status");
+ require(bytes(txHash).length > 0, "Missing Tron tx hash");
+ require(finalizedBlock >= minFinalityBlocks, "Insufficient finality");
+ require(fulfillmentId != bytes32(0), "Missing fulfillment id");
+ require(!usedFulfillmentIds[fulfillmentId], "Fulfillment already used");
+
+ usedFulfillmentIds[fulfillmentId] = true;
+ fulfillmentIdsByRequest[requestId] = fulfillmentId;
+ txHashes[requestId] = txHash;
+ finalizedBlocks[requestId] = finalizedBlock;
+ request.status = BridgeStatus.Confirmed;
+ request.completedAt = block.timestamp;
+
+ emit TronBridgeConfirmed(requestId, txHash, finalizedBlock, fulfillmentId);
+ }
+
+ function markFailed(bytes32 requestId, string calldata reason) external onlyRole(ORACLE_ROLE) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Locked, "Invalid status");
+
+ request.status = BridgeStatus.Failed;
+ request.completedAt = block.timestamp;
+
+ emit TronBridgeMarkedFailed(requestId, reason);
+ }
+
+ function getBridgeStatus(bytes32 requestId) external view override returns (BridgeRequest memory) {
+ return bridgeRequests[requestId];
+ }
+
+ function cancelBridge(bytes32 requestId) external override nonReentrant returns (bool) {
+ BridgeRequest storage request = bridgeRequests[requestId];
+ require(request.status == BridgeStatus.Pending || request.status == BridgeStatus.Locked, "Cannot cancel");
+ require(msg.sender == request.sender, "Not request sender");
+
+ if (request.token == address(0)) {
+ payable(request.sender).transfer(request.amount);
+ } else {
+ IERC20(request.token).safeTransfer(request.sender, request.amount);
+ }
+
+ request.status = BridgeStatus.Cancelled;
+ request.completedAt = block.timestamp;
+ return true;
+ }
+
+ function estimateFee(
+ address,
+ uint256,
+ bytes calldata
+ ) external pure override returns (uint256 fee) {
+ return 1000000000000000;
+ }
+
+ function setIsActive(bool _isActive) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ isActive = _isActive;
+ }
+
+ function setMinFinalityBlocks(uint256 blocks_) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ require(blocks_ > 0, "Zero finality");
+ minFinalityBlocks = blocks_;
+ }
+}
diff --git a/contracts/dex/DODOPMMIntegration.sol b/contracts/dex/DODOPMMIntegration.sol
index 13c406d..c1a6bbb 100644
--- a/contracts/dex/DODOPMMIntegration.sol
+++ b/contracts/dex/DODOPMMIntegration.sol
@@ -637,10 +637,16 @@ contract DODOPMMIntegration is AccessControl, ReentrancyGuard {
require(IDODOPMMPool(pool)._BASE_TOKEN_() == expectedBaseToken, "DODOPMMIntegration: unexpected base token");
require(IDODOPMMPool(pool)._QUOTE_TOKEN_() == expectedQuoteToken, "DODOPMMIntegration: unexpected quote token");
- IDODOPMMPool(pool).getVaultReserve();
- IDODOPMMPool(pool).getMidPrice();
+ (uint256 baseReserve, uint256 quoteReserve) = IDODOPMMPool(pool).getVaultReserve();
IDODOPMMPool(pool)._BASE_RESERVE_();
IDODOPMMPool(pool)._QUOTE_RESERVE_();
+
+ // Freshly created pools have zero reserves until the first buyShares/addLiquidity call.
+ // Skip getMidPrice on empty pools because some DODO deployments revert during PMM math
+ // before the initial seed has established target reserves.
+ if (baseReserve > 0 && quoteReserve > 0) {
+ IDODOPMMPool(pool).getMidPrice();
+ }
}
function _setStandardPoolSurface(
diff --git a/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol b/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol
new file mode 100644
index 0000000..efbb656
--- /dev/null
+++ b/contracts/dex/recovered/RecoveredWave1PoolFamily2682.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+/// @notice Recovered verbatim Wave 1 runtime family artifact.
+/// @dev Generated by scripts/verify/recover-wave1-pool-source-families.py from live chain bytecode.
+contract RecoveredWave1PoolFamily2682 {
+ constructor() {
+ bytes memory runtime = hex"6040608081526004908136101561001557600080fd5b600090813560e01c928363199ab5fe146103da578363217a4b70146103b257836336223ce91461038a5783634a248d2a146103635783634c85b425146101ee5783636bdb678a146101ce57508263796da7af146100e05782637d7215041461010a5782639da771f414610197578263ab44a7a3146101b4578263bbf5ce7814610197578263c55dae6314610171578263d4b970461461014a578263d7a2e4c914610127578263dfdf2a721461010a578263ec2fd46d146100e557505063ee27c689146100e057600080fd5b6103f7565b34610106578160031936011261010657602090516706f05b59d3b200008152f35b5080fd5b346101065781600319360112610106576020906002549051908152f35b3461010657602036600319011261010657602090610143610626565b9051908152f35b3461010657816003193601126101065760015490516001600160a01b039091168152602090f35b34610106578160031936011261010657905490516001600160a01b039091168152602090f35b346101065781600319360112610106576020906003549051908152f35b346101065781600319360112610106576020905160038152f35b346101ea57826003193601126101ea5760209250549051908152f35b8280fd5b90346101ea576020908160031936011261035f576001600160a01b0383358181160361035b57808554169382519184836024816370a0823160e01b998a825230878301525afa92831561035157908591889461031c575b5060015416956024855180988193825230868301525afa9485156103125786956102db575b506060955061027b60025483610481565b9461028860035482610481565b928060025581600355151590816102d1575b506102c1575b506102b36102ae82866104bb565b610889565b928251948552840152820152f35b6102c961076b565b9055856102a0565b905015158761029a565b9094508381813d831161030b575b6102f38183610449565b81010312610307576060955051938661026a565b8580fd5b503d6102e9565b83513d88823e3d90fd5b8281939295503d831161034a575b6103348183610449565b81010312610346578490519288610245565b8680fd5b503d61032a565b84513d89823e3d90fd5b8480fd5b8380fd5b5034610106578160031936011261010657905490516001600160a01b039091168152602090f35b5090346103af57806003193601126103af575060025460035482519182526020820152f35b80fd5b503461010657816003193601126101065760015490516001600160a01b039091168152602090f35b5034610106576020366003190112610106576020906101436104fb565b3461041a57600036600319011261041a57602061041261041f565b604051908152f35b600080fd5b60025415801561043f575b6104395761043661076b565b90565b60045490565b506003541561042a565b90601f8019910116810190811067ffffffffffffffff82111761046b57604052565b634e487b7160e01b600052604160045260246000fd5b9190820391821161048e57565b634e487b7160e01b600052601160045260246000fd5b9061270d9182810292818404149015171561048e57565b8181029291811591840414171561048e57565b9190820180921161048e57565b81156104e5570490565b634e487b7160e01b600052601260045260246000fd5b600080546040516370a0823160e01b81523060048201529291906001600160a01b03906020908590602490829085165afa93841561061b5782946105e8575b506105486002548095610481565b91821580156105de575b6105d057610584612710610565856104a4565b049561057e60035497610578818a6104bb565b926104ce565b906104db565b948086116105d6575b5084156105d05750906105a9846105b1936001541633906108d3565b6002546104ce565b6002556105c082600354610481565b6003556105cb61076b565b600455565b93505050565b94503861058d565b5060035415610552565b9093506020813d8211610613575b8161060360209383610449565b810103126101065751923861053a565b3d91506105f6565b6040513d84823e3d90fd5b6001546040516370a0823160e01b815230600482015291906001600160a01b03906020908490602490829085165afa92831561073557600093610702575b506106726003548094610481565b90811580156106f8575b6106e8576106a261271061068f846104a4565b049461057e6002549661057881896104bb565b938085116106f0575b5083156106e857906106c6846106ce936000541633906108d3565b6003546104ce565b6003556106dd82600254610481565b6002556105cb61076b565b506000925050565b9350386106ab565b506002541561067c565b90926020823d821161072d575b8161071c60209383610449565b810103126103af5750519138610664565b3d915061070f565b6040513d6000823e3d90fd5b9081602091031261041a575160ff8116810361041a5790565b60ff16604d811161048e57600a0a90565b6000805460405163313ce56760e01b808252602093926001600160a01b039085908490600490829085165afa92831561087e57908591859461085f575b50600154169160046040518094819382525afa938415610854578394610825575b505060035491670de0b6b3a764000092838102938185041490151715610811575061080461043693926107fe61057e9361075a565b906104bb565b916107fe6002549161075a565b634e487b7160e01b81526011600452602490fd5b610845929450803d1061084d575b61083d8183610449565b810190610741565b9138806107c9565b503d610833565b6040513d85823e3d90fd5b610877919450823d841161084d5761083d8183610449565b92386107a8565b6040513d86823e3d90fd5b9081156108cd57600180830180841161048e57811c90835b8483106108ad57505050565b909193506108c4846108bf81846104db565b6104ce565b821c91906108a1565b60009150565b60405163a9059cbb60e01b602082019081526001600160a01b039384166024830152604480830195909552938152929167ffffffffffffffff91608085018381118682101761046b576040521692600080938192519082875af13d156109d4573d9182116109c0579061096891604051916109586020601f19601f8401160184610449565b82523d84602084013e5b846109e1565b908151918215159283610998575b5050506109805750565b60249060405190635274afe760e01b82526004820152fd5b8192935090602091810103126101065760200151908115918215036103af5750388080610976565b634e487b7160e01b83526041600452602483fd5b6109689150606090610962565b90610a0857508051156109f657805190602001fd5b604051630a12f52160e11b8152600490fd5b81511580610a3b575b610a19575090565b604051639996b31560e01b81526001600160a01b039091166004820152602490fd5b50803b15610a1156fea2646970667358221220bfbb45d7d38da668495affb4fb18c85f1b1b7a8bca2e5cf5f677952e715df40264736f6c63430008140033";
+ assembly {
+ return(add(runtime, 0x20), mload(runtime))
+ }
+ }
+}
diff --git a/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol b/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol
new file mode 100644
index 0000000..4e35057
--- /dev/null
+++ b/contracts/dex/recovered/RecoveredWave1PoolFamily668.sol
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+/// @notice Recovered verbatim Wave 1 runtime family artifact.
+/// @dev Generated by scripts/verify/recover-wave1-pool-source-families.py from live chain bytecode.
+contract RecoveredWave1PoolFamily668 {
+ constructor() {
+ bytes memory runtime = hex"604060808152600436101561001357600080fd5b6000803560e01c918263199ab5fe14610227578263217a4b701461012857826336223ce9146102005782634a248d2a1461014f5782634c85b425146101c95782636bdb678a146101ac578263796da7af146100dc5782637d721504146101065782639da771f414610175578263ab44a7a314610192578263bbf5ce7814610175578263c55dae631461014f578263d4b9704614610128578263d7a2e4c914610123578263dfdf2a7214610106578263ec2fd46d146100e157505063ee27c689146100dc57600080fd5b610248565b34610102578160031936011261010257602090516706f05b59d3b200008152f35b5080fd5b346101025781600319360112610102576020906002549051908152f35b610227565b3461010257816003193601126101025760015490516001600160a01b039091168152602090f35b34610102578160031936011261010257905490516001600160a01b039091168152602090f35b346101025781600319360112610102576020906003549051908152f35b346101025781600319360112610102576020905160038152f35b346101025781600319360112610102576020906004549051908152f35b34610102576020366003190112610102576004356001600160a01b0381160361010257606091815191818352816020840152820152f35b90346102245780600319360112610224575060025460035482519182526020820152f35b80fd5b3461024357602036600319011261024357602060405160008152f35b600080fd5b34610243576000366003190112610243576020600454604051908152f3fea2646970667358221220ced0ca29ab61872de6b0da1d116f43badd5e311e57c167714eeca3094a72269d64736f6c63430008140033";
+ assembly {
+ return(add(runtime, 0x20), mload(runtime))
+ }
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/LICENSE b/contracts/vendor/sushiswap-v2/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
diff --git a/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol b/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol
new file mode 100644
index 0000000..7ed393c
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/UniswapV2ERC20.sol
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+import './libraries/SafeMath.sol';
+
+contract UniswapV2ERC20 {
+ using SafeMathUniswap for uint;
+
+ string public constant name = 'SushiSwap LP Token';
+ string public constant symbol = 'SLP';
+ uint8 public constant decimals = 18;
+ uint public totalSupply;
+ mapping(address => uint) public balanceOf;
+ mapping(address => mapping(address => uint)) public allowance;
+
+ bytes32 public DOMAIN_SEPARATOR;
+ // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
+ bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
+ mapping(address => uint) public nonces;
+
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ constructor() public {
+ uint chainId;
+ assembly {
+ chainId := chainid()
+ }
+ DOMAIN_SEPARATOR = keccak256(
+ abi.encode(
+ keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
+ keccak256(bytes(name)),
+ keccak256(bytes('1')),
+ chainId,
+ address(this)
+ )
+ );
+ }
+
+ function _mint(address to, uint value) internal {
+ totalSupply = totalSupply.add(value);
+ balanceOf[to] = balanceOf[to].add(value);
+ emit Transfer(address(0), to, value);
+ }
+
+ function _burn(address from, uint value) internal {
+ balanceOf[from] = balanceOf[from].sub(value);
+ totalSupply = totalSupply.sub(value);
+ emit Transfer(from, address(0), value);
+ }
+
+ function _approve(address owner, address spender, uint value) private {
+ allowance[owner][spender] = value;
+ emit Approval(owner, spender, value);
+ }
+
+ function _transfer(address from, address to, uint value) private {
+ balanceOf[from] = balanceOf[from].sub(value);
+ balanceOf[to] = balanceOf[to].add(value);
+ emit Transfer(from, to, value);
+ }
+
+ function approve(address spender, uint value) external returns (bool) {
+ _approve(msg.sender, spender, value);
+ return true;
+ }
+
+ function transfer(address to, uint value) external returns (bool) {
+ _transfer(msg.sender, to, value);
+ return true;
+ }
+
+ function transferFrom(address from, address to, uint value) external returns (bool) {
+ if (allowance[from][msg.sender] != uint(-1)) {
+ allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
+ }
+ _transfer(from, to, value);
+ return true;
+ }
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
+ require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
+ bytes32 digest = keccak256(
+ abi.encodePacked(
+ '\x19\x01',
+ DOMAIN_SEPARATOR,
+ keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
+ )
+ );
+ address recoveredAddress = ecrecover(digest, v, r, s);
+ require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
+ _approve(owner, spender, value);
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol b/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol
new file mode 100644
index 0000000..f0a56fc
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/UniswapV2Factory.sol
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+import './interfaces/IUniswapV2Factory.sol';
+import './UniswapV2Pair.sol';
+
+contract UniswapV2Factory is IUniswapV2Factory {
+ address public override feeTo;
+ address public override feeToSetter;
+ address public override migrator;
+
+ mapping(address => mapping(address => address)) public override getPair;
+ address[] public override allPairs;
+
+ event PairCreated(address indexed token0, address indexed token1, address pair, uint);
+
+ constructor(address _feeToSetter) public {
+ feeToSetter = _feeToSetter;
+ }
+
+ function allPairsLength() external override view returns (uint) {
+ return allPairs.length;
+ }
+
+ function pairCodeHash() external pure returns (bytes32) {
+ return keccak256(type(UniswapV2Pair).creationCode);
+ }
+
+ function createPair(address tokenA, address tokenB) external override returns (address pair) {
+ require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
+ (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
+ require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
+ require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
+ bytes memory bytecode = type(UniswapV2Pair).creationCode;
+ bytes32 salt = keccak256(abi.encodePacked(token0, token1));
+ assembly {
+ pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
+ }
+ UniswapV2Pair(pair).initialize(token0, token1);
+ getPair[token0][token1] = pair;
+ getPair[token1][token0] = pair; // populate mapping in the reverse direction
+ allPairs.push(pair);
+ emit PairCreated(token0, token1, pair, allPairs.length);
+ }
+
+ function setFeeTo(address _feeTo) external override {
+ require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
+ feeTo = _feeTo;
+ }
+
+ function setMigrator(address _migrator) external override {
+ require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
+ migrator = _migrator;
+ }
+
+ function setFeeToSetter(address _feeToSetter) external override {
+ require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
+ feeToSetter = _feeToSetter;
+ }
+
+}
diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol b/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol
new file mode 100644
index 0000000..408cde6
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/UniswapV2Pair.sol
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+import './UniswapV2ERC20.sol';
+import './libraries/Math.sol';
+import './libraries/UQ112x112.sol';
+import './interfaces/IERC20.sol';
+import './interfaces/IUniswapV2Factory.sol';
+import './interfaces/IUniswapV2Callee.sol';
+
+interface IMigrator {
+ // Return the desired amount of liquidity token that the migrator wants.
+ function desiredLiquidity() external view returns (uint256);
+}
+
+contract UniswapV2Pair is UniswapV2ERC20 {
+ using SafeMathUniswap for uint;
+ using UQ112x112 for uint224;
+
+ uint public constant MINIMUM_LIQUIDITY = 10**3;
+ bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
+
+ address public factory;
+ address public token0;
+ address public token1;
+
+ uint112 private reserve0; // uses single storage slot, accessible via getReserves
+ uint112 private reserve1; // uses single storage slot, accessible via getReserves
+ uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
+
+ uint public price0CumulativeLast;
+ uint public price1CumulativeLast;
+ uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
+
+ uint private unlocked = 1;
+ modifier lock() {
+ require(unlocked == 1, 'UniswapV2: LOCKED');
+ unlocked = 0;
+ _;
+ unlocked = 1;
+ }
+
+ function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
+ _reserve0 = reserve0;
+ _reserve1 = reserve1;
+ _blockTimestampLast = blockTimestampLast;
+ }
+
+ function _safeTransfer(address token, address to, uint value) private {
+ (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
+ require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
+ }
+
+ event Mint(address indexed sender, uint amount0, uint amount1);
+ event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
+ event Swap(
+ address indexed sender,
+ uint amount0In,
+ uint amount1In,
+ uint amount0Out,
+ uint amount1Out,
+ address indexed to
+ );
+ event Sync(uint112 reserve0, uint112 reserve1);
+
+ constructor() public {
+ factory = msg.sender;
+ }
+
+ // called once by the factory at time of deployment
+ function initialize(address _token0, address _token1) external {
+ require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
+ token0 = _token0;
+ token1 = _token1;
+ }
+
+ // update reserves and, on the first call per block, price accumulators
+ function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
+ require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
+ uint32 blockTimestamp = uint32(block.timestamp % 2**32);
+ uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
+ if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
+ // * never overflows, and + overflow is desired
+ price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
+ price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
+ }
+ reserve0 = uint112(balance0);
+ reserve1 = uint112(balance1);
+ blockTimestampLast = blockTimestamp;
+ emit Sync(reserve0, reserve1);
+ }
+
+ // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
+ function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
+ address feeTo = IUniswapV2Factory(factory).feeTo();
+ feeOn = feeTo != address(0);
+ uint _kLast = kLast; // gas savings
+ if (feeOn) {
+ if (_kLast != 0) {
+ uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
+ uint rootKLast = Math.sqrt(_kLast);
+ if (rootK > rootKLast) {
+ uint numerator = totalSupply.mul(rootK.sub(rootKLast));
+ uint denominator = rootK.mul(5).add(rootKLast);
+ uint liquidity = numerator / denominator;
+ if (liquidity > 0) _mint(feeTo, liquidity);
+ }
+ }
+ } else if (_kLast != 0) {
+ kLast = 0;
+ }
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function mint(address to) external lock returns (uint liquidity) {
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ uint balance0 = IERC20Uniswap(token0).balanceOf(address(this));
+ uint balance1 = IERC20Uniswap(token1).balanceOf(address(this));
+ uint amount0 = balance0.sub(_reserve0);
+ uint amount1 = balance1.sub(_reserve1);
+
+ bool feeOn = _mintFee(_reserve0, _reserve1);
+ uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
+ if (_totalSupply == 0) {
+ address migrator = IUniswapV2Factory(factory).migrator();
+ if (msg.sender == migrator) {
+ liquidity = IMigrator(migrator).desiredLiquidity();
+ require(liquidity > 0 && liquidity != uint256(-1), "Bad desired liquidity");
+ } else {
+ require(migrator == address(0), "Must not have migrator");
+ liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
+ _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
+ }
+ } else {
+ liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
+ }
+ require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
+ _mint(to, liquidity);
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
+ emit Mint(msg.sender, amount0, amount1);
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function burn(address to) external lock returns (uint amount0, uint amount1) {
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ address _token0 = token0; // gas savings
+ address _token1 = token1; // gas savings
+ uint balance0 = IERC20Uniswap(_token0).balanceOf(address(this));
+ uint balance1 = IERC20Uniswap(_token1).balanceOf(address(this));
+ uint liquidity = balanceOf[address(this)];
+
+ bool feeOn = _mintFee(_reserve0, _reserve1);
+ uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
+ amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
+ amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
+ require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
+ _burn(address(this), liquidity);
+ _safeTransfer(_token0, to, amount0);
+ _safeTransfer(_token1, to, amount1);
+ balance0 = IERC20Uniswap(_token0).balanceOf(address(this));
+ balance1 = IERC20Uniswap(_token1).balanceOf(address(this));
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
+ emit Burn(msg.sender, amount0, amount1, to);
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
+ require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
+
+ uint balance0;
+ uint balance1;
+ { // scope for _token{0,1}, avoids stack too deep errors
+ address _token0 = token0;
+ address _token1 = token1;
+ require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
+ if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
+ if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
+ if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
+ balance0 = IERC20Uniswap(_token0).balanceOf(address(this));
+ balance1 = IERC20Uniswap(_token1).balanceOf(address(this));
+ }
+ uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
+ uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
+ require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
+ { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
+ uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
+ uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
+ require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
+ }
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
+ }
+
+ // force balances to match reserves
+ function skim(address to) external lock {
+ address _token0 = token0; // gas savings
+ address _token1 = token1; // gas savings
+ _safeTransfer(_token0, to, IERC20Uniswap(_token0).balanceOf(address(this)).sub(reserve0));
+ _safeTransfer(_token1, to, IERC20Uniswap(_token1).balanceOf(address(this)).sub(reserve1));
+ }
+
+ // force reserves to match balances
+ function sync() external lock {
+ _update(IERC20Uniswap(token0).balanceOf(address(this)), IERC20Uniswap(token1).balanceOf(address(this)), reserve0, reserve1);
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol b/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol
new file mode 100644
index 0000000..c873a09
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/UniswapV2Router02.sol
@@ -0,0 +1,447 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+import './libraries/UniswapV2Library.sol';
+import './libraries/SafeMath.sol';
+import './libraries/TransferHelper.sol';
+import './interfaces/IUniswapV2Router02.sol';
+import './interfaces/IUniswapV2Factory.sol';
+import './interfaces/IERC20.sol';
+import './interfaces/IWETH.sol';
+
+contract UniswapV2Router02 is IUniswapV2Router02 {
+ using SafeMathUniswap for uint;
+
+ address public immutable override factory;
+ address public immutable override WETH;
+
+ modifier ensure(uint deadline) {
+ require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
+ _;
+ }
+
+ constructor(address _factory, address _WETH) public {
+ factory = _factory;
+ WETH = _WETH;
+ }
+
+ receive() external payable {
+ assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
+ }
+
+ // **** ADD LIQUIDITY ****
+ function _addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin
+ ) internal virtual returns (uint amountA, uint amountB) {
+ // create the pair if it doesn't exist yet
+ if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
+ IUniswapV2Factory(factory).createPair(tokenA, tokenB);
+ }
+ (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
+ if (reserveA == 0 && reserveB == 0) {
+ (amountA, amountB) = (amountADesired, amountBDesired);
+ } else {
+ uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
+ if (amountBOptimal <= amountBDesired) {
+ require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
+ (amountA, amountB) = (amountADesired, amountBOptimal);
+ } else {
+ uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
+ assert(amountAOptimal <= amountADesired);
+ require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
+ (amountA, amountB) = (amountAOptimal, amountBDesired);
+ }
+ }
+ }
+ function addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
+ (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
+ TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
+ liquidity = IUniswapV2Pair(pair).mint(to);
+ }
+ function addLiquidityETH(
+ address token,
+ uint amountTokenDesired,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
+ (amountToken, amountETH) = _addLiquidity(
+ token,
+ WETH,
+ amountTokenDesired,
+ msg.value,
+ amountTokenMin,
+ amountETHMin
+ );
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
+ IWETH(WETH).deposit{value: amountETH}();
+ assert(IWETH(WETH).transfer(pair, amountETH));
+ liquidity = IUniswapV2Pair(pair).mint(to);
+ // refund dust eth, if any
+ if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
+ }
+
+ // **** REMOVE LIQUIDITY ****
+ function removeLiquidity(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
+ (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
+ (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
+ (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
+ require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
+ require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
+ }
+ function removeLiquidityETH(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
+ (amountToken, amountETH) = removeLiquidity(
+ token,
+ WETH,
+ liquidity,
+ amountTokenMin,
+ amountETHMin,
+ address(this),
+ deadline
+ );
+ TransferHelper.safeTransfer(token, to, amountToken);
+ IWETH(WETH).withdraw(amountETH);
+ TransferHelper.safeTransferETH(to, amountETH);
+ }
+ function removeLiquidityWithPermit(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountA, uint amountB) {
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
+ }
+ function removeLiquidityETHWithPermit(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountToken, uint amountETH) {
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
+ }
+
+ // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
+ function removeLiquidityETHSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountETH) {
+ (, amountETH) = removeLiquidity(
+ token,
+ WETH,
+ liquidity,
+ amountTokenMin,
+ amountETHMin,
+ address(this),
+ deadline
+ );
+ TransferHelper.safeTransfer(token, to, IERC20Uniswap(token).balanceOf(address(this)));
+ IWETH(WETH).withdraw(amountETH);
+ TransferHelper.safeTransferETH(to, amountETH);
+ }
+ function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountETH) {
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
+ token, liquidity, amountTokenMin, amountETHMin, to, deadline
+ );
+ }
+
+ // **** SWAP ****
+ // requires the initial amount to have already been sent to the first pair
+ function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
+ for (uint i; i < path.length - 1; i++) {
+ (address input, address output) = (path[i], path[i + 1]);
+ (address token0,) = UniswapV2Library.sortTokens(input, output);
+ uint amountOut = amounts[i + 1];
+ (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
+ address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
+ IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
+ amount0Out, amount1Out, to, new bytes(0)
+ );
+ }
+ }
+ function swapExactTokensForTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
+ amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, to);
+ }
+ function swapTokensForExactTokens(
+ uint amountOut,
+ uint amountInMax,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, to);
+ }
+ function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ IWETH(WETH).deposit{value: amounts[0]}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
+ _swap(amounts, path, to);
+ }
+ function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, address(this));
+ IWETH(WETH).withdraw(amounts[amounts.length - 1]);
+ TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
+ }
+ function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, address(this));
+ IWETH(WETH).withdraw(amounts[amounts.length - 1]);
+ TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
+ }
+ function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ IWETH(WETH).deposit{value: amounts[0]}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
+ _swap(amounts, path, to);
+ // refund dust eth, if any
+ if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
+ }
+
+ // **** SWAP (supporting fee-on-transfer tokens) ****
+ // requires the initial amount to have already been sent to the first pair
+ function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
+ for (uint i; i < path.length - 1; i++) {
+ (address input, address output) = (path[i], path[i + 1]);
+ (address token0,) = UniswapV2Library.sortTokens(input, output);
+ IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
+ uint amountInput;
+ uint amountOutput;
+ { // scope to avoid stack too deep errors
+ (uint reserve0, uint reserve1,) = pair.getReserves();
+ (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
+ amountInput = IERC20Uniswap(input).balanceOf(address(pair)).sub(reserveInput);
+ amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
+ }
+ (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
+ address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
+ pair.swap(amount0Out, amount1Out, to, new bytes(0));
+ }
+ }
+ function swapExactTokensForTokensSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) {
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
+ );
+ uint balanceBefore = IERC20Uniswap(path[path.length - 1]).balanceOf(to);
+ _swapSupportingFeeOnTransferTokens(path, to);
+ require(
+ IERC20Uniswap(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
+ 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
+ );
+ }
+ function swapExactETHForTokensSupportingFeeOnTransferTokens(
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ )
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ uint amountIn = msg.value;
+ IWETH(WETH).deposit{value: amountIn}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
+ uint balanceBefore = IERC20Uniswap(path[path.length - 1]).balanceOf(to);
+ _swapSupportingFeeOnTransferTokens(path, to);
+ require(
+ IERC20Uniswap(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
+ 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
+ );
+ }
+ function swapExactTokensForETHSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ )
+ external
+ virtual
+ override
+ ensure(deadline)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
+ );
+ _swapSupportingFeeOnTransferTokens(path, address(this));
+ uint amountOut = IERC20Uniswap(WETH).balanceOf(address(this));
+ require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ IWETH(WETH).withdraw(amountOut);
+ TransferHelper.safeTransferETH(to, amountOut);
+ }
+
+ // **** LIBRARY FUNCTIONS ****
+ function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
+ return UniswapV2Library.quote(amountA, reserveA, reserveB);
+ }
+
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
+ public
+ pure
+ virtual
+ override
+ returns (uint amountOut)
+ {
+ return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
+ }
+
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
+ public
+ pure
+ virtual
+ override
+ returns (uint amountIn)
+ {
+ return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
+ }
+
+ function getAmountsOut(uint amountIn, address[] memory path)
+ public
+ view
+ virtual
+ override
+ returns (uint[] memory amounts)
+ {
+ return UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ }
+
+ function getAmountsIn(uint amountOut, address[] memory path)
+ public
+ view
+ virtual
+ override
+ returns (uint[] memory amounts)
+ {
+ return UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol b/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol
new file mode 100644
index 0000000..fd7b169
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IERC20.sol
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IERC20Uniswap {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external view returns (string memory);
+ function symbol() external view returns (string memory);
+ function decimals() external view returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+}
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol
new file mode 100644
index 0000000..a11b1c4
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Callee.sol
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Callee {
+ function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
+}
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol
new file mode 100644
index 0000000..41fb595
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2ERC20.sol
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IUniswapV2ERC20 {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external pure returns (string memory);
+ function symbol() external pure returns (string memory);
+ function decimals() external pure returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+
+ function DOMAIN_SEPARATOR() external view returns (bytes32);
+ function PERMIT_TYPEHASH() external pure returns (bytes32);
+ function nonces(address owner) external view returns (uint);
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
+}
\ No newline at end of file
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol
new file mode 100644
index 0000000..aea4477
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Factory.sol
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Factory {
+ event PairCreated(address indexed token0, address indexed token1, address pair, uint);
+
+ function feeTo() external view returns (address);
+ function feeToSetter() external view returns (address);
+ function migrator() external view returns (address);
+
+ function getPair(address tokenA, address tokenB) external view returns (address pair);
+ function allPairs(uint) external view returns (address pair);
+ function allPairsLength() external view returns (uint);
+
+ function createPair(address tokenA, address tokenB) external returns (address pair);
+
+ function setFeeTo(address) external;
+ function setFeeToSetter(address) external;
+ function setMigrator(address) external;
+}
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol
new file mode 100644
index 0000000..9c69f70
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Pair.sol
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Pair {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external pure returns (string memory);
+ function symbol() external pure returns (string memory);
+ function decimals() external pure returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+
+ function DOMAIN_SEPARATOR() external view returns (bytes32);
+ function PERMIT_TYPEHASH() external pure returns (bytes32);
+ function nonces(address owner) external view returns (uint);
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
+
+ event Mint(address indexed sender, uint amount0, uint amount1);
+ event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
+ event Swap(
+ address indexed sender,
+ uint amount0In,
+ uint amount1In,
+ uint amount0Out,
+ uint amount1Out,
+ address indexed to
+ );
+ event Sync(uint112 reserve0, uint112 reserve1);
+
+ function MINIMUM_LIQUIDITY() external pure returns (uint);
+ function factory() external view returns (address);
+ function token0() external view returns (address);
+ function token1() external view returns (address);
+ function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
+ function price0CumulativeLast() external view returns (uint);
+ function price1CumulativeLast() external view returns (uint);
+ function kLast() external view returns (uint);
+
+ function mint(address to) external returns (uint liquidity);
+ function burn(address to) external returns (uint amount0, uint amount1);
+ function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
+ function skim(address to) external;
+ function sync() external;
+
+ function initialize(address, address) external;
+}
\ No newline at end of file
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol
new file mode 100644
index 0000000..095f895
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router01.sol
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.6.2;
+
+interface IUniswapV2Router01 {
+ function factory() external pure returns (address);
+ function WETH() external pure returns (address);
+
+ function addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountA, uint amountB, uint liquidity);
+ function addLiquidityETH(
+ address token,
+ uint amountTokenDesired,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external payable returns (uint amountToken, uint amountETH, uint liquidity);
+ function removeLiquidity(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountA, uint amountB);
+ function removeLiquidityETH(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountToken, uint amountETH);
+ function removeLiquidityWithPermit(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountA, uint amountB);
+ function removeLiquidityETHWithPermit(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountToken, uint amountETH);
+ function swapExactTokensForTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external returns (uint[] memory amounts);
+ function swapTokensForExactTokens(
+ uint amountOut,
+ uint amountInMax,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external returns (uint[] memory amounts);
+ function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ payable
+ returns (uint[] memory amounts);
+ function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
+ external
+ returns (uint[] memory amounts);
+ function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ returns (uint[] memory amounts);
+ function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
+ external
+ payable
+ returns (uint[] memory amounts);
+
+ function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
+ function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
+ function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts);
+}
\ No newline at end of file
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol
new file mode 100644
index 0000000..4fca80d
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IUniswapV2Router02.sol
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.6.2;
+
+import './IUniswapV2Router01.sol';
+
+interface IUniswapV2Router02 is IUniswapV2Router01 {
+ function removeLiquidityETHSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountETH);
+ function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountETH);
+
+ function swapExactTokensForTokensSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external;
+ function swapExactETHForTokensSupportingFeeOnTransferTokens(
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external payable;
+ function swapExactTokensForETHSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external;
+}
\ No newline at end of file
diff --git a/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol b/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol
new file mode 100644
index 0000000..775da06
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/interfaces/IWETH.sol
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+interface IWETH {
+ function deposit() external payable;
+ function transfer(address to, uint value) external returns (bool);
+ function withdraw(uint) external;
+}
\ No newline at end of file
diff --git a/contracts/vendor/sushiswap-v2/libraries/Math.sol b/contracts/vendor/sushiswap-v2/libraries/Math.sol
new file mode 100644
index 0000000..d183d80
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/libraries/Math.sol
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+// a library for performing various math operations
+
+library Math {
+ function min(uint x, uint y) internal pure returns (uint z) {
+ z = x < y ? x : y;
+ }
+
+ // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
+ function sqrt(uint y) internal pure returns (uint z) {
+ if (y > 3) {
+ z = y;
+ uint x = y / 2 + 1;
+ while (x < z) {
+ z = x;
+ x = (y / x + x) / 2;
+ }
+ } else if (y != 0) {
+ z = 1;
+ }
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol b/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol
new file mode 100644
index 0000000..1cc199b
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/libraries/SafeMath.sol
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math)
+
+library SafeMathUniswap {
+ function add(uint x, uint y) internal pure returns (uint z) {
+ require((z = x + y) >= x, 'ds-math-add-overflow');
+ }
+
+ function sub(uint x, uint y) internal pure returns (uint z) {
+ require((z = x - y) <= x, 'ds-math-sub-underflow');
+ }
+
+ function mul(uint x, uint y) internal pure returns (uint z) {
+ require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol b/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol
new file mode 100644
index 0000000..d179a67
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/libraries/TransferHelper.sol
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.6.0;
+
+// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
+library TransferHelper {
+ function safeApprove(address token, address to, uint value) internal {
+ // bytes4(keccak256(bytes('approve(address,uint256)')));
+ (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
+ require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED');
+ }
+
+ function safeTransfer(address token, address to, uint value) internal {
+ // bytes4(keccak256(bytes('transfer(address,uint256)')));
+ (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
+ require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED');
+ }
+
+ function safeTransferFrom(address token, address from, address to, uint value) internal {
+ // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
+ (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
+ require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');
+ }
+
+ function safeTransferETH(address to, uint value) internal {
+ (bool success,) = to.call{value:value}(new bytes(0));
+ require(success, 'TransferHelper: ETH_TRANSFER_FAILED');
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol b/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol
new file mode 100644
index 0000000..9af8f74
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/libraries/UQ112x112.sol
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity =0.6.12;
+
+// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))
+
+// range: [0, 2**112 - 1]
+// resolution: 1 / 2**112
+
+library UQ112x112 {
+ uint224 constant Q112 = 2**112;
+
+ // encode a uint112 as a UQ112x112
+ function encode(uint112 y) internal pure returns (uint224 z) {
+ z = uint224(y) * Q112; // never overflows
+ }
+
+ // divide a UQ112x112 by a uint112, returning a UQ112x112
+ function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
+ z = x / uint224(y);
+ }
+}
diff --git a/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol b/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol
new file mode 100644
index 0000000..14cffb0
--- /dev/null
+++ b/contracts/vendor/sushiswap-v2/libraries/UniswapV2Library.sol
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-3.0
+
+pragma solidity >=0.5.0;
+
+import '../interfaces/IUniswapV2Pair.sol';
+
+import "./SafeMath.sol";
+
+library UniswapV2Library {
+ using SafeMathUniswap for uint;
+
+ // returns sorted token addresses, used to handle return values from pairs sorted in this order
+ function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
+ require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
+ (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
+ require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
+ }
+
+ // calculates the CREATE2 address for a pair without making any external calls
+ function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
+ (address token0, address token1) = sortTokens(tokenA, tokenB);
+ pair = address(uint(keccak256(abi.encodePacked(
+ hex'ff',
+ factory,
+ keccak256(abi.encodePacked(token0, token1)),
+ hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303' // init code hash
+ ))));
+ }
+
+ // fetches and sorts the reserves for a pair
+ function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
+ (address token0,) = sortTokens(tokenA, tokenB);
+ (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
+ (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
+ }
+
+ // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
+ function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
+ require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
+ require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ amountB = amountA.mul(reserveB) / reserveA;
+ }
+
+ // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
+ require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
+ require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ uint amountInWithFee = amountIn.mul(997);
+ uint numerator = amountInWithFee.mul(reserveOut);
+ uint denominator = reserveIn.mul(1000).add(amountInWithFee);
+ amountOut = numerator / denominator;
+ }
+
+ // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
+ require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
+ require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ uint numerator = reserveIn.mul(amountOut).mul(1000);
+ uint denominator = reserveOut.sub(amountOut).mul(997);
+ amountIn = (numerator / denominator).add(1);
+ }
+
+ // performs chained getAmountOut calculations on any number of pairs
+ function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
+ require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
+ amounts = new uint[](path.length);
+ amounts[0] = amountIn;
+ for (uint i; i < path.length - 1; i++) {
+ (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
+ amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
+ }
+ }
+
+ // performs chained getAmountIn calculations on any number of pairs
+ function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
+ require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
+ amounts = new uint[](path.length);
+ amounts[amounts.length - 1] = amountOut;
+ for (uint i = path.length - 1; i > 0; i--) {
+ (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
+ amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
+ }
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol b/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol
new file mode 100644
index 0000000..404a8be
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/UniswapV2ERC20.sol
@@ -0,0 +1,94 @@
+pragma solidity =0.5.16;
+
+import './interfaces/IUniswapV2ERC20.sol';
+import './libraries/SafeMath.sol';
+
+contract UniswapV2ERC20 is IUniswapV2ERC20 {
+ using SafeMath for uint;
+
+ string public constant name = 'Uniswap V2';
+ string public constant symbol = 'UNI-V2';
+ uint8 public constant decimals = 18;
+ uint public totalSupply;
+ mapping(address => uint) public balanceOf;
+ mapping(address => mapping(address => uint)) public allowance;
+
+ bytes32 public DOMAIN_SEPARATOR;
+ // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
+ bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
+ mapping(address => uint) public nonces;
+
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ constructor() public {
+ uint chainId;
+ assembly {
+ chainId := chainid
+ }
+ DOMAIN_SEPARATOR = keccak256(
+ abi.encode(
+ keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
+ keccak256(bytes(name)),
+ keccak256(bytes('1')),
+ chainId,
+ address(this)
+ )
+ );
+ }
+
+ function _mint(address to, uint value) internal {
+ totalSupply = totalSupply.add(value);
+ balanceOf[to] = balanceOf[to].add(value);
+ emit Transfer(address(0), to, value);
+ }
+
+ function _burn(address from, uint value) internal {
+ balanceOf[from] = balanceOf[from].sub(value);
+ totalSupply = totalSupply.sub(value);
+ emit Transfer(from, address(0), value);
+ }
+
+ function _approve(address owner, address spender, uint value) private {
+ allowance[owner][spender] = value;
+ emit Approval(owner, spender, value);
+ }
+
+ function _transfer(address from, address to, uint value) private {
+ balanceOf[from] = balanceOf[from].sub(value);
+ balanceOf[to] = balanceOf[to].add(value);
+ emit Transfer(from, to, value);
+ }
+
+ function approve(address spender, uint value) external returns (bool) {
+ _approve(msg.sender, spender, value);
+ return true;
+ }
+
+ function transfer(address to, uint value) external returns (bool) {
+ _transfer(msg.sender, to, value);
+ return true;
+ }
+
+ function transferFrom(address from, address to, uint value) external returns (bool) {
+ if (allowance[from][msg.sender] != uint(-1)) {
+ allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
+ }
+ _transfer(from, to, value);
+ return true;
+ }
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
+ require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
+ bytes32 digest = keccak256(
+ abi.encodePacked(
+ '\x19\x01',
+ DOMAIN_SEPARATOR,
+ keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
+ )
+ );
+ address recoveredAddress = ecrecover(digest, v, r, s);
+ require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
+ _approve(owner, spender, value);
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol b/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol
new file mode 100644
index 0000000..a66ad82
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol
@@ -0,0 +1,49 @@
+pragma solidity =0.5.16;
+
+import './interfaces/IUniswapV2Factory.sol';
+import './UniswapV2Pair.sol';
+
+contract UniswapV2Factory is IUniswapV2Factory {
+ address public feeTo;
+ address public feeToSetter;
+
+ mapping(address => mapping(address => address)) public getPair;
+ address[] public allPairs;
+
+ event PairCreated(address indexed token0, address indexed token1, address pair, uint);
+
+ constructor(address _feeToSetter) public {
+ feeToSetter = _feeToSetter;
+ }
+
+ function allPairsLength() external view returns (uint) {
+ return allPairs.length;
+ }
+
+ function createPair(address tokenA, address tokenB) external returns (address pair) {
+ require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
+ (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
+ require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
+ require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
+ bytes memory bytecode = type(UniswapV2Pair).creationCode;
+ bytes32 salt = keccak256(abi.encodePacked(token0, token1));
+ assembly {
+ pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
+ }
+ IUniswapV2Pair(pair).initialize(token0, token1);
+ getPair[token0][token1] = pair;
+ getPair[token1][token0] = pair; // populate mapping in the reverse direction
+ allPairs.push(pair);
+ emit PairCreated(token0, token1, pair, allPairs.length);
+ }
+
+ function setFeeTo(address _feeTo) external {
+ require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
+ feeTo = _feeTo;
+ }
+
+ function setFeeToSetter(address _feeToSetter) external {
+ require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
+ feeToSetter = _feeToSetter;
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol b/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol
new file mode 100644
index 0000000..f87a1db
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol
@@ -0,0 +1,201 @@
+pragma solidity =0.5.16;
+
+import './interfaces/IUniswapV2Pair.sol';
+import './UniswapV2ERC20.sol';
+import './libraries/Math.sol';
+import './libraries/UQ112x112.sol';
+import './interfaces/IERC20.sol';
+import './interfaces/IUniswapV2Factory.sol';
+import './interfaces/IUniswapV2Callee.sol';
+
+contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
+ using SafeMath for uint;
+ using UQ112x112 for uint224;
+
+ uint public constant MINIMUM_LIQUIDITY = 10**3;
+ bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
+
+ address public factory;
+ address public token0;
+ address public token1;
+
+ uint112 private reserve0; // uses single storage slot, accessible via getReserves
+ uint112 private reserve1; // uses single storage slot, accessible via getReserves
+ uint32 private blockTimestampLast; // uses single storage slot, accessible via getReserves
+
+ uint public price0CumulativeLast;
+ uint public price1CumulativeLast;
+ uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity event
+
+ uint private unlocked = 1;
+ modifier lock() {
+ require(unlocked == 1, 'UniswapV2: LOCKED');
+ unlocked = 0;
+ _;
+ unlocked = 1;
+ }
+
+ function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
+ _reserve0 = reserve0;
+ _reserve1 = reserve1;
+ _blockTimestampLast = blockTimestampLast;
+ }
+
+ function _safeTransfer(address token, address to, uint value) private {
+ (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
+ require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
+ }
+
+ event Mint(address indexed sender, uint amount0, uint amount1);
+ event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
+ event Swap(
+ address indexed sender,
+ uint amount0In,
+ uint amount1In,
+ uint amount0Out,
+ uint amount1Out,
+ address indexed to
+ );
+ event Sync(uint112 reserve0, uint112 reserve1);
+
+ constructor() public {
+ factory = msg.sender;
+ }
+
+ // called once by the factory at time of deployment
+ function initialize(address _token0, address _token1) external {
+ require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
+ token0 = _token0;
+ token1 = _token1;
+ }
+
+ // update reserves and, on the first call per block, price accumulators
+ function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
+ require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
+ uint32 blockTimestamp = uint32(block.timestamp % 2**32);
+ uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
+ if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
+ // * never overflows, and + overflow is desired
+ price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
+ price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
+ }
+ reserve0 = uint112(balance0);
+ reserve1 = uint112(balance1);
+ blockTimestampLast = blockTimestamp;
+ emit Sync(reserve0, reserve1);
+ }
+
+ // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
+ function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
+ address feeTo = IUniswapV2Factory(factory).feeTo();
+ feeOn = feeTo != address(0);
+ uint _kLast = kLast; // gas savings
+ if (feeOn) {
+ if (_kLast != 0) {
+ uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
+ uint rootKLast = Math.sqrt(_kLast);
+ if (rootK > rootKLast) {
+ uint numerator = totalSupply.mul(rootK.sub(rootKLast));
+ uint denominator = rootK.mul(5).add(rootKLast);
+ uint liquidity = numerator / denominator;
+ if (liquidity > 0) _mint(feeTo, liquidity);
+ }
+ }
+ } else if (_kLast != 0) {
+ kLast = 0;
+ }
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function mint(address to) external lock returns (uint liquidity) {
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ uint balance0 = IERC20(token0).balanceOf(address(this));
+ uint balance1 = IERC20(token1).balanceOf(address(this));
+ uint amount0 = balance0.sub(_reserve0);
+ uint amount1 = balance1.sub(_reserve1);
+
+ bool feeOn = _mintFee(_reserve0, _reserve1);
+ uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
+ if (_totalSupply == 0) {
+ liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
+ _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
+ } else {
+ liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
+ }
+ require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
+ _mint(to, liquidity);
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
+ emit Mint(msg.sender, amount0, amount1);
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function burn(address to) external lock returns (uint amount0, uint amount1) {
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ address _token0 = token0; // gas savings
+ address _token1 = token1; // gas savings
+ uint balance0 = IERC20(_token0).balanceOf(address(this));
+ uint balance1 = IERC20(_token1).balanceOf(address(this));
+ uint liquidity = balanceOf[address(this)];
+
+ bool feeOn = _mintFee(_reserve0, _reserve1);
+ uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
+ amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
+ amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
+ require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
+ _burn(address(this), liquidity);
+ _safeTransfer(_token0, to, amount0);
+ _safeTransfer(_token1, to, amount1);
+ balance0 = IERC20(_token0).balanceOf(address(this));
+ balance1 = IERC20(_token1).balanceOf(address(this));
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
+ emit Burn(msg.sender, amount0, amount1, to);
+ }
+
+ // this low-level function should be called from a contract which performs important safety checks
+ function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
+ require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
+ (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
+ require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
+
+ uint balance0;
+ uint balance1;
+ { // scope for _token{0,1}, avoids stack too deep errors
+ address _token0 = token0;
+ address _token1 = token1;
+ require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
+ if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
+ if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
+ if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
+ balance0 = IERC20(_token0).balanceOf(address(this));
+ balance1 = IERC20(_token1).balanceOf(address(this));
+ }
+ uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
+ uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
+ require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
+ { // scope for reserve{0,1}Adjusted, avoids stack too deep errors
+ uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
+ uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
+ require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
+ }
+
+ _update(balance0, balance1, _reserve0, _reserve1);
+ emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
+ }
+
+ // force balances to match reserves
+ function skim(address to) external lock {
+ address _token0 = token0; // gas savings
+ address _token1 = token1; // gas savings
+ _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
+ _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
+ }
+
+ // force reserves to match balances
+ function sync() external lock {
+ _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol b/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol
new file mode 100644
index 0000000..c1e8c3e
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/interfaces/IERC20.sol
@@ -0,0 +1,17 @@
+pragma solidity >=0.5.0;
+
+interface IERC20 {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external view returns (string memory);
+ function symbol() external view returns (string memory);
+ function decimals() external view returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+}
diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol
new file mode 100644
index 0000000..f3910ab
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Callee.sol
@@ -0,0 +1,5 @@
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Callee {
+ function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
+}
diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol
new file mode 100644
index 0000000..8718931
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2ERC20.sol
@@ -0,0 +1,23 @@
+pragma solidity >=0.5.0;
+
+interface IUniswapV2ERC20 {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external pure returns (string memory);
+ function symbol() external pure returns (string memory);
+ function decimals() external pure returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+
+ function DOMAIN_SEPARATOR() external view returns (bytes32);
+ function PERMIT_TYPEHASH() external pure returns (bytes32);
+ function nonces(address owner) external view returns (uint);
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
+}
diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol
new file mode 100644
index 0000000..e73dc59
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Factory.sol
@@ -0,0 +1,17 @@
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Factory {
+ event PairCreated(address indexed token0, address indexed token1, address pair, uint);
+
+ function feeTo() external view returns (address);
+ function feeToSetter() external view returns (address);
+
+ function getPair(address tokenA, address tokenB) external view returns (address pair);
+ function allPairs(uint) external view returns (address pair);
+ function allPairsLength() external view returns (uint);
+
+ function createPair(address tokenA, address tokenB) external returns (address pair);
+
+ function setFeeTo(address) external;
+ function setFeeToSetter(address) external;
+}
diff --git a/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol
new file mode 100644
index 0000000..762dd4d
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/interfaces/IUniswapV2Pair.sol
@@ -0,0 +1,52 @@
+pragma solidity >=0.5.0;
+
+interface IUniswapV2Pair {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external pure returns (string memory);
+ function symbol() external pure returns (string memory);
+ function decimals() external pure returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+
+ function DOMAIN_SEPARATOR() external view returns (bytes32);
+ function PERMIT_TYPEHASH() external pure returns (bytes32);
+ function nonces(address owner) external view returns (uint);
+
+ function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external;
+
+ event Mint(address indexed sender, uint amount0, uint amount1);
+ event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
+ event Swap(
+ address indexed sender,
+ uint amount0In,
+ uint amount1In,
+ uint amount0Out,
+ uint amount1Out,
+ address indexed to
+ );
+ event Sync(uint112 reserve0, uint112 reserve1);
+
+ function MINIMUM_LIQUIDITY() external pure returns (uint);
+ function factory() external view returns (address);
+ function token0() external view returns (address);
+ function token1() external view returns (address);
+ function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
+ function price0CumulativeLast() external view returns (uint);
+ function price1CumulativeLast() external view returns (uint);
+ function kLast() external view returns (uint);
+
+ function mint(address to) external returns (uint liquidity);
+ function burn(address to) external returns (uint amount0, uint amount1);
+ function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
+ function skim(address to) external;
+ function sync() external;
+
+ function initialize(address, address) external;
+}
diff --git a/contracts/vendor/uniswap-v2-core/libraries/Math.sol b/contracts/vendor/uniswap-v2-core/libraries/Math.sol
new file mode 100644
index 0000000..1cab10d
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/libraries/Math.sol
@@ -0,0 +1,23 @@
+pragma solidity =0.5.16;
+
+// a library for performing various math operations
+
+library Math {
+ function min(uint x, uint y) internal pure returns (uint z) {
+ z = x < y ? x : y;
+ }
+
+ // babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
+ function sqrt(uint y) internal pure returns (uint z) {
+ if (y > 3) {
+ z = y;
+ uint x = y / 2 + 1;
+ while (x < z) {
+ z = x;
+ x = (y / x + x) / 2;
+ }
+ } else if (y != 0) {
+ z = 1;
+ }
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol b/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol
new file mode 100644
index 0000000..f2fbe16
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/libraries/SafeMath.sol
@@ -0,0 +1,17 @@
+pragma solidity =0.5.16;
+
+// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math)
+
+library SafeMath {
+ function add(uint x, uint y) internal pure returns (uint z) {
+ require((z = x + y) >= x, 'ds-math-add-overflow');
+ }
+
+ function sub(uint x, uint y) internal pure returns (uint z) {
+ require((z = x - y) <= x, 'ds-math-sub-underflow');
+ }
+
+ function mul(uint x, uint y) internal pure returns (uint z) {
+ require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol b/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol
new file mode 100644
index 0000000..a453f72
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-core/libraries/UQ112x112.sol
@@ -0,0 +1,20 @@
+pragma solidity =0.5.16;
+
+// a library for handling binary fixed point numbers (https://en.wikipedia.org/wiki/Q_(number_format))
+
+// range: [0, 2**112 - 1]
+// resolution: 1 / 2**112
+
+library UQ112x112 {
+ uint224 constant Q112 = 2**112;
+
+ // encode a uint112 as a UQ112x112
+ function encode(uint112 y) internal pure returns (uint224 z) {
+ z = uint224(y) * Q112; // never overflows
+ }
+
+ // divide a UQ112x112 by a uint112, returning a UQ112x112
+ function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
+ z = x / uint224(y);
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol b/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol
new file mode 100644
index 0000000..23ff4b0
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol
@@ -0,0 +1,446 @@
+pragma solidity =0.6.6;
+
+import '../uniswap-v2-core/interfaces/IUniswapV2Factory.sol';
+import '../sushiswap-v2/libraries/TransferHelper.sol';
+
+import './interfaces/IUniswapV2Router02.sol';
+import './libraries/UniswapV2Library.sol';
+import './libraries/SafeMath.sol';
+import './interfaces/IERC20.sol';
+import './interfaces/IWETH.sol';
+
+contract UniswapV2Router02 is IUniswapV2Router02 {
+ using SafeMath for uint;
+
+ address public immutable override factory;
+ address public immutable override WETH;
+
+ modifier ensure(uint deadline) {
+ require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
+ _;
+ }
+
+ constructor(address _factory, address _WETH) public {
+ factory = _factory;
+ WETH = _WETH;
+ }
+
+ receive() external payable {
+ assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
+ }
+
+ // **** ADD LIQUIDITY ****
+ function _addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin
+ ) internal virtual returns (uint amountA, uint amountB) {
+ // create the pair if it doesn't exist yet
+ if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
+ IUniswapV2Factory(factory).createPair(tokenA, tokenB);
+ }
+ (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
+ if (reserveA == 0 && reserveB == 0) {
+ (amountA, amountB) = (amountADesired, amountBDesired);
+ } else {
+ uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
+ if (amountBOptimal <= amountBDesired) {
+ require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
+ (amountA, amountB) = (amountADesired, amountBOptimal);
+ } else {
+ uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
+ assert(amountAOptimal <= amountADesired);
+ require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
+ (amountA, amountB) = (amountAOptimal, amountBDesired);
+ }
+ }
+ }
+ function addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
+ (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
+ TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
+ liquidity = IUniswapV2Pair(pair).mint(to);
+ }
+ function addLiquidityETH(
+ address token,
+ uint amountTokenDesired,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
+ (amountToken, amountETH) = _addLiquidity(
+ token,
+ WETH,
+ amountTokenDesired,
+ msg.value,
+ amountTokenMin,
+ amountETHMin
+ );
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
+ IWETH(WETH).deposit{value: amountETH}();
+ assert(IWETH(WETH).transfer(pair, amountETH));
+ liquidity = IUniswapV2Pair(pair).mint(to);
+ // refund dust eth, if any
+ if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
+ }
+
+ // **** REMOVE LIQUIDITY ****
+ function removeLiquidity(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
+ (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
+ (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
+ (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
+ require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
+ require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
+ }
+ function removeLiquidityETH(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
+ (amountToken, amountETH) = removeLiquidity(
+ token,
+ WETH,
+ liquidity,
+ amountTokenMin,
+ amountETHMin,
+ address(this),
+ deadline
+ );
+ TransferHelper.safeTransfer(token, to, amountToken);
+ IWETH(WETH).withdraw(amountETH);
+ TransferHelper.safeTransferETH(to, amountETH);
+ }
+ function removeLiquidityWithPermit(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountA, uint amountB) {
+ address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
+ }
+ function removeLiquidityETHWithPermit(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountToken, uint amountETH) {
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
+ }
+
+ // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
+ function removeLiquidityETHSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) public virtual override ensure(deadline) returns (uint amountETH) {
+ (, amountETH) = removeLiquidity(
+ token,
+ WETH,
+ liquidity,
+ amountTokenMin,
+ amountETHMin,
+ address(this),
+ deadline
+ );
+ TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
+ IWETH(WETH).withdraw(amountETH);
+ TransferHelper.safeTransferETH(to, amountETH);
+ }
+ function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external virtual override returns (uint amountETH) {
+ address pair = UniswapV2Library.pairFor(factory, token, WETH);
+ uint value = approveMax ? uint(-1) : liquidity;
+ IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
+ amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
+ token, liquidity, amountTokenMin, amountETHMin, to, deadline
+ );
+ }
+
+ // **** SWAP ****
+ // requires the initial amount to have already been sent to the first pair
+ function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
+ for (uint i; i < path.length - 1; i++) {
+ (address input, address output) = (path[i], path[i + 1]);
+ (address token0,) = UniswapV2Library.sortTokens(input, output);
+ uint amountOut = amounts[i + 1];
+ (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
+ address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
+ IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
+ amount0Out, amount1Out, to, new bytes(0)
+ );
+ }
+ }
+ function swapExactTokensForTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
+ amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, to);
+ }
+ function swapTokensForExactTokens(
+ uint amountOut,
+ uint amountInMax,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, to);
+ }
+ function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ IWETH(WETH).deposit{value: amounts[0]}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
+ _swap(amounts, path, to);
+ }
+ function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, address(this));
+ IWETH(WETH).withdraw(amounts[amounts.length - 1]);
+ TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
+ }
+ function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
+ );
+ _swap(amounts, path, address(this));
+ IWETH(WETH).withdraw(amounts[amounts.length - 1]);
+ TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
+ }
+ function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ returns (uint[] memory amounts)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
+ IWETH(WETH).deposit{value: amounts[0]}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
+ _swap(amounts, path, to);
+ // refund dust eth, if any
+ if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
+ }
+
+ // **** SWAP (supporting fee-on-transfer tokens) ****
+ // requires the initial amount to have already been sent to the first pair
+ function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
+ for (uint i; i < path.length - 1; i++) {
+ (address input, address output) = (path[i], path[i + 1]);
+ (address token0,) = UniswapV2Library.sortTokens(input, output);
+ IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
+ uint amountInput;
+ uint amountOutput;
+ { // scope to avoid stack too deep errors
+ (uint reserve0, uint reserve1,) = pair.getReserves();
+ (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
+ amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
+ amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
+ }
+ (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
+ address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
+ pair.swap(amount0Out, amount1Out, to, new bytes(0));
+ }
+ }
+ function swapExactTokensForTokensSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external virtual override ensure(deadline) {
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
+ );
+ uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
+ _swapSupportingFeeOnTransferTokens(path, to);
+ require(
+ IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
+ 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
+ );
+ }
+ function swapExactETHForTokensSupportingFeeOnTransferTokens(
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ )
+ external
+ virtual
+ override
+ payable
+ ensure(deadline)
+ {
+ require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
+ uint amountIn = msg.value;
+ IWETH(WETH).deposit{value: amountIn}();
+ assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
+ uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
+ _swapSupportingFeeOnTransferTokens(path, to);
+ require(
+ IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
+ 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
+ );
+ }
+ function swapExactTokensForETHSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ )
+ external
+ virtual
+ override
+ ensure(deadline)
+ {
+ require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
+ TransferHelper.safeTransferFrom(
+ path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
+ );
+ _swapSupportingFeeOnTransferTokens(path, address(this));
+ uint amountOut = IERC20(WETH).balanceOf(address(this));
+ require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
+ IWETH(WETH).withdraw(amountOut);
+ TransferHelper.safeTransferETH(to, amountOut);
+ }
+
+ // **** LIBRARY FUNCTIONS ****
+ function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
+ return UniswapV2Library.quote(amountA, reserveA, reserveB);
+ }
+
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
+ public
+ pure
+ virtual
+ override
+ returns (uint amountOut)
+ {
+ return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
+ }
+
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
+ public
+ pure
+ virtual
+ override
+ returns (uint amountIn)
+ {
+ return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
+ }
+
+ function getAmountsOut(uint amountIn, address[] memory path)
+ public
+ view
+ virtual
+ override
+ returns (uint[] memory amounts)
+ {
+ return UniswapV2Library.getAmountsOut(factory, amountIn, path);
+ }
+
+ function getAmountsIn(uint amountOut, address[] memory path)
+ public
+ view
+ virtual
+ override
+ returns (uint[] memory amounts)
+ {
+ return UniswapV2Library.getAmountsIn(factory, amountOut, path);
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol
new file mode 100644
index 0000000..c1e8c3e
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IERC20.sol
@@ -0,0 +1,17 @@
+pragma solidity >=0.5.0;
+
+interface IERC20 {
+ event Approval(address indexed owner, address indexed spender, uint value);
+ event Transfer(address indexed from, address indexed to, uint value);
+
+ function name() external view returns (string memory);
+ function symbol() external view returns (string memory);
+ function decimals() external view returns (uint8);
+ function totalSupply() external view returns (uint);
+ function balanceOf(address owner) external view returns (uint);
+ function allowance(address owner, address spender) external view returns (uint);
+
+ function approve(address spender, uint value) external returns (bool);
+ function transfer(address to, uint value) external returns (bool);
+ function transferFrom(address from, address to, uint value) external returns (bool);
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol
new file mode 100644
index 0000000..4619967
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router01.sol
@@ -0,0 +1,95 @@
+pragma solidity >=0.6.2;
+
+interface IUniswapV2Router01 {
+ function factory() external pure returns (address);
+ function WETH() external pure returns (address);
+
+ function addLiquidity(
+ address tokenA,
+ address tokenB,
+ uint amountADesired,
+ uint amountBDesired,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountA, uint amountB, uint liquidity);
+ function addLiquidityETH(
+ address token,
+ uint amountTokenDesired,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external payable returns (uint amountToken, uint amountETH, uint liquidity);
+ function removeLiquidity(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountA, uint amountB);
+ function removeLiquidityETH(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountToken, uint amountETH);
+ function removeLiquidityWithPermit(
+ address tokenA,
+ address tokenB,
+ uint liquidity,
+ uint amountAMin,
+ uint amountBMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountA, uint amountB);
+ function removeLiquidityETHWithPermit(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountToken, uint amountETH);
+ function swapExactTokensForTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external returns (uint[] memory amounts);
+ function swapTokensForExactTokens(
+ uint amountOut,
+ uint amountInMax,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external returns (uint[] memory amounts);
+ function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ payable
+ returns (uint[] memory amounts);
+ function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
+ external
+ returns (uint[] memory amounts);
+ function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
+ external
+ returns (uint[] memory amounts);
+ function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
+ external
+ payable
+ returns (uint[] memory amounts);
+
+ function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB);
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut);
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn);
+ function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts);
+ function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts);
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol
new file mode 100644
index 0000000..1fc7b0a
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IUniswapV2Router02.sol
@@ -0,0 +1,44 @@
+pragma solidity >=0.6.2;
+
+import './IUniswapV2Router01.sol';
+
+interface IUniswapV2Router02 is IUniswapV2Router01 {
+ function removeLiquidityETHSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline
+ ) external returns (uint amountETH);
+ function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
+ address token,
+ uint liquidity,
+ uint amountTokenMin,
+ uint amountETHMin,
+ address to,
+ uint deadline,
+ bool approveMax, uint8 v, bytes32 r, bytes32 s
+ ) external returns (uint amountETH);
+
+ function swapExactTokensForTokensSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external;
+ function swapExactETHForTokensSupportingFeeOnTransferTokens(
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external payable;
+ function swapExactTokensForETHSupportingFeeOnTransferTokens(
+ uint amountIn,
+ uint amountOutMin,
+ address[] calldata path,
+ address to,
+ uint deadline
+ ) external;
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol b/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol
new file mode 100644
index 0000000..e05fb77
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/interfaces/IWETH.sol
@@ -0,0 +1,7 @@
+pragma solidity >=0.5.0;
+
+interface IWETH {
+ function deposit() external payable;
+ function transfer(address to, uint value) external returns (bool);
+ function withdraw(uint) external;
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol b/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol
new file mode 100644
index 0000000..ba6fc21
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/libraries/SafeMath.sol
@@ -0,0 +1,17 @@
+pragma solidity =0.6.6;
+
+// a library for performing overflow-safe math, courtesy of DappHub (https://github.com/dapphub/ds-math)
+
+library SafeMath {
+ function add(uint x, uint y) internal pure returns (uint z) {
+ require((z = x + y) >= x, 'ds-math-add-overflow');
+ }
+
+ function sub(uint x, uint y) internal pure returns (uint z) {
+ require((z = x - y) <= x, 'ds-math-sub-underflow');
+ }
+
+ function mul(uint x, uint y) internal pure returns (uint z) {
+ require(y == 0 || (z = x * y) / y == x, 'ds-math-mul-overflow');
+ }
+}
diff --git a/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol b/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol
new file mode 100644
index 0000000..e5fb8c7
--- /dev/null
+++ b/contracts/vendor/uniswap-v2-periphery/libraries/UniswapV2Library.sol
@@ -0,0 +1,82 @@
+pragma solidity >=0.5.0;
+
+import '../../uniswap-v2-core/interfaces/IUniswapV2Pair.sol';
+
+import "./SafeMath.sol";
+
+library UniswapV2Library {
+ using SafeMath for uint;
+
+ // returns sorted token addresses, used to handle return values from pairs sorted in this order
+ function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
+ require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
+ (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
+ require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
+ }
+
+ // calculates the CREATE2 address for a pair without making any external calls
+ function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
+ (address token0, address token1) = sortTokens(tokenA, tokenB);
+ pair = address(uint(keccak256(abi.encodePacked(
+ hex'ff',
+ factory,
+ keccak256(abi.encodePacked(token0, token1)),
+ hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // init code hash
+ ))));
+ }
+
+ // fetches and sorts the reserves for a pair
+ function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
+ (address token0,) = sortTokens(tokenA, tokenB);
+ (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
+ (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
+ }
+
+ // given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
+ function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
+ require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
+ require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ amountB = amountA.mul(reserveB) / reserveA;
+ }
+
+ // given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
+ function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
+ require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
+ require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ uint amountInWithFee = amountIn.mul(997);
+ uint numerator = amountInWithFee.mul(reserveOut);
+ uint denominator = reserveIn.mul(1000).add(amountInWithFee);
+ amountOut = numerator / denominator;
+ }
+
+ // given an output amount of an asset and pair reserves, returns a required input amount of the other asset
+ function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
+ require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
+ require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
+ uint numerator = reserveIn.mul(amountOut).mul(1000);
+ uint denominator = reserveOut.sub(amountOut).mul(997);
+ amountIn = (numerator / denominator).add(1);
+ }
+
+ // performs chained getAmountOut calculations on any number of pairs
+ function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
+ require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
+ amounts = new uint[](path.length);
+ amounts[0] = amountIn;
+ for (uint i; i < path.length - 1; i++) {
+ (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
+ amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
+ }
+ }
+
+ // performs chained getAmountIn calculations on any number of pairs
+ function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
+ require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
+ amounts = new uint[](path.length);
+ amounts[amounts.length - 1] = amountOut;
+ for (uint i = path.length - 1; i > 0; i--) {
+ (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
+ amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
+ }
+ }
+}
diff --git a/contracts/wrapped-lp-public/Chain138LPLocker.sol b/contracts/wrapped-lp-public/Chain138LPLocker.sol
new file mode 100644
index 0000000..3e95d02
--- /dev/null
+++ b/contracts/wrapped-lp-public/Chain138LPLocker.sol
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import "@openzeppelin/contracts/access/AccessControl.sol";
+import "./interfaces/IWLPProgramEvents.sol";
+
+/**
+ * @title Chain138LPLocker
+ * @notice Escrows DODO PMM LP ERC20 on Chain 138; releases only to BRIDGE_RELEASE_ROLE (Option A).
+ * @dev Per-lock `lockRef` is used as the cross-chain idempotency key for destination mint.
+ * One deployment typically corresponds to one `lpToken` (one pool’s LP token).
+ */
+contract Chain138LPLocker is AccessControl, IWLPProgramEvents {
+ using SafeERC20 for IERC20;
+
+ bytes32 public constant BRIDGE_RELEASE_ROLE = keccak256("BRIDGE_RELEASE_ROLE");
+
+ IERC20 public immutable lpToken;
+
+ struct Deposit {
+ address depositor;
+ uint256 amount;
+ bool released;
+ }
+
+ uint256 public lockCounter;
+ uint256 public totalEscrowed;
+
+ mapping(bytes32 => Deposit) public deposits;
+
+ constructor(address lpToken_, address admin) {
+ require(lpToken_ != address(0) && admin != address(0), "Locker: zero");
+ lpToken = IERC20(lpToken_);
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ }
+
+ /**
+ * @notice Lock LP into escrow; `lockRef` must be relayed to the public chain for mint.
+ */
+ function deposit(uint256 amount) external returns (bytes32 lockRef) {
+ require(amount > 0, "Locker: zero amount");
+ lpToken.safeTransferFrom(msg.sender, address(this), amount);
+ lockRef = keccak256(abi.encode(block.chainid, address(this), lockCounter++, msg.sender, amount));
+ deposits[lockRef] = Deposit({depositor: msg.sender, amount: amount, released: false});
+ totalEscrowed += amount;
+ emit LPLocked(lockRef, msg.sender, amount, address(lpToken));
+ }
+
+ /**
+ * @notice Release one full lock to `to` after redemption message (or operational unwind).
+ */
+ function releaseLock(bytes32 lockRef, address to) external onlyRole(BRIDGE_RELEASE_ROLE) {
+ Deposit storage d = deposits[lockRef];
+ require(d.amount > 0 && !d.released, "Locker: bad lock");
+ d.released = true;
+ uint256 amt = d.amount;
+ totalEscrowed -= amt;
+ lpToken.safeTransfer(to, amt);
+ emit LPReleased(lockRef, to, amt, address(lpToken));
+ }
+
+ /**
+ * @notice Aggregate release for pro-rata / FIFO policies (requires off-chain ordering).
+ */
+ function releaseAmount(address to, uint256 amount) external onlyRole(BRIDGE_RELEASE_ROLE) {
+ require(amount > 0 && amount <= totalEscrowed, "Locker: amount");
+ totalEscrowed -= amount;
+ lpToken.safeTransfer(to, amount);
+ emit LPReleased(bytes32(0), to, amount, address(lpToken));
+ }
+}
diff --git a/contracts/wrapped-lp-public/PublicChainMintController.sol b/contracts/wrapped-lp-public/PublicChainMintController.sol
new file mode 100644
index 0000000..9787a52
--- /dev/null
+++ b/contracts/wrapped-lp-public/PublicChainMintController.sol
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/access/AccessControl.sol";
+import "./WLPReceiptToken.sol";
+import "./interfaces/IWLPProgramEvents.sol";
+
+/**
+ * @title PublicChainMintController
+ * @notice Destination-chain controller: mints `wLP` once per `lockRef` (replay protection).
+ * @dev Relayer must verify Chain 138 `LPLocked` event / attestation off-chain or via future ZK proof.
+ */
+contract PublicChainMintController is AccessControl, IWLPProgramEvents {
+ bytes32 public constant RELAYER_ROLE = keccak256("RELAYER_ROLE");
+
+ WLPReceiptToken public immutable wlp;
+ /// @notice Optional Chain 138 locker address for documentation / future cross-verify hooks.
+ address public immutable chain138Locker;
+
+ mapping(bytes32 => bool) public mintedForLock;
+ bool public mintPaused;
+
+ constructor(address wlp_, address chain138Locker_, address admin) {
+ require(wlp_ != address(0) && admin != address(0), "MintCtl: zero");
+ wlp = WLPReceiptToken(wlp_);
+ chain138Locker = chain138Locker_;
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ }
+
+ /**
+ * @notice Idempotent mint keyed by `lockRef` (must match Chain 138 locker emission).
+ */
+ function mintForLock(bytes32 lockRef, address recipient, uint256 amount) external onlyRole(RELAYER_ROLE) {
+ require(!mintPaused, "MintCtl: paused");
+ require(lockRef != bytes32(0), "MintCtl: zero lockRef");
+ require(recipient != address(0) && amount > 0, "MintCtl: bad args");
+ require(!mintedForLock[lockRef], "MintCtl: replay");
+ mintedForLock[lockRef] = true;
+ wlp.mint(recipient, amount);
+ emit WLPMinted(lockRef, recipient, amount);
+ }
+
+ function setMintPaused(bool paused) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ mintPaused = paused;
+ }
+}
diff --git a/contracts/wrapped-lp-public/README.md b/contracts/wrapped-lp-public/README.md
new file mode 100644
index 0000000..b8e7e2a
--- /dev/null
+++ b/contracts/wrapped-lp-public/README.md
@@ -0,0 +1,92 @@
+# Wrapped LP / Public Chain Program (Scaffold)
+
+Solidity for **Option A** (lock → mint `wLP` → redeem) and **Option B** (ERC-4626 vault + NAV oracle). This is a **new vertical**: **not** an extension of `CWMultiTokenBridgeL1/L2` or catalog **cW*** CCIP paths.
+
+---
+
+## Documentation (exhaustive index)
+
+| Doc | Description |
+|-----|-------------|
+| [WRAPPED_LP_PROGRAM_REFERENCE.md](../../../docs/04-configuration/WRAPPED_LP_PROGRAM_REFERENCE.md) | **Master index**, invariants, glossary, phase gates |
+| [ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md](../../../docs/03-deployment/ADR_WRAPPED_LP_VAULT_PUBLIC_CHAIN.md) | Architecture decisions, alternatives |
+| [WRAPPED_LP_MESSAGE_SCHEMA.md](../../../docs/04-configuration/WRAPPED_LP_MESSAGE_SCHEMA.md) | `lockRef`, payloads, CCIP, redemption |
+| [WRAPPED_LP_ORACLE_STACK.md](../../../docs/04-configuration/WRAPPED_LP_ORACLE_STACK.md) | Two-price model, keepers, lending |
+| [WRAPPED_LP_LENDING_GOVERNANCE.md](../../../docs/04-configuration/WRAPPED_LP_LENDING_GOVERNANCE.md) | Venues, listing, risk matrix |
+| [WRAPPED_LP_OPERATIONS_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_OPERATIONS_RUNBOOK.md) | SLOs, RACI, incidents |
+| [WRAPPED_LP_AUDIT_CHECKLIST.md](../../../docs/03-deployment/WRAPPED_LP_AUDIT_CHECKLIST.md) | Auditor scope, threats |
+
+---
+
+## Contracts
+
+| File | Role |
+|------|------|
+| `Chain138LPLocker.sol` | Escrow LP on 138; `lockRef`; `releaseLock` / `releaseAmount` |
+| `WLPReceiptToken.sol` | Public-chain `wLP` (MINTER/BURNER) |
+| `PublicChainMintController.sol` | Idempotent `mintForLock`; `mintPaused` |
+| `WLPRedemptionGateway.sol` | Burn + `RedemptionRequested` |
+| `WrappedLPNAVVault.sol` | ERC-4626 + `depositCap` |
+| `WLPNAVOracle.sol` | USD-style feed, heartbeat, breaker, `isStale()` |
+| `interfaces/IWLPProgramEvents.sol` | Shared events |
+
+---
+
+## Build / test (scoped Forge)
+
+Full-repo `forge test` may fail on legacy **0.5.x/0.6.x** vendor trees; always use **scope**:
+
+```bash
+cd smom-dbis-138
+bash scripts/forge/scope.sh build wrapped-lp-public
+bash scripts/forge/scope.sh test wrapped-lp-public --match-path 'test/wrapped-lp-public/*.t.sol'
+```
+
+---
+
+## Deployment
+
+| Script | Use |
+|--------|-----|
+| [DeployWrappedLPLockerChain138.s.sol](../../script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol) | **Chain 138 only** — `Chain138LPLocker` |
+| [DeployWrappedLPPublicChain.s.sol](../../script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol) | **Public chain** — wLP, mint controller, gateway, oracle, optional vault |
+| [DeployWrappedLPProgram.s.sol](../../script/wrapped-lp-public/DeployWrappedLPProgram.s.sol) | **Single-chain / Anvil** — full stack on one RPC (not production two-chain) |
+
+Production: two `forge script` runs (138 RPC then destination RPC). See [WRAPPED_LP_DEPLOYMENT_RUNBOOK.md](../../../docs/03-deployment/WRAPPED_LP_DEPLOYMENT_RUNBOOK.md).
+
+### Post-deploy role wiring (required)
+
+| Grant | To |
+|-------|-----|
+| `WLPReceiptToken.MINTER_ROLE` | `PublicChainMintController` |
+| `WLPReceiptToken.BURNER_ROLE` | `WLPRedemptionGateway` |
+| `PublicChainMintController.RELAYER_ROLE` | Relayer worker(s) |
+| `Chain138LPLocker.BRIDGE_RELEASE_ROLE` | Relayer (138 release path) |
+| `WLPNAVOracle.KEEPER_ROLE` | Price keeper |
+
+Use **multisig** for `DEFAULT_ADMIN_ROLE` on production.
+
+### Environment variables (deploy script)
+
+| Variable | Purpose |
+|----------|---------|
+| `LP_TOKEN` | DODO LP ERC20 on 138 |
+| `ADMIN` | Multisig / admin |
+| `PRIVATE_KEY` | Broadcast key (lab only) |
+| `USDC` | Optional; if set, deploys `WrappedLPNAVVault` |
+
+---
+
+## Limitations (read before mainnet)
+
+1. **Relayer trust:** Mint path assumes **honest** relayer until ZK/attestation hardening.
+2. **Fungible wLP:** Secondary trading breaks **simple** per-lock redemption; see message schema doc.
+3. **Vault:** `totalAssets()` follows **ERC-20 balance**; off-chain strategy on 138 is **operational**.
+
+---
+
+## Repo context
+
+- PMM pools: `docs/11-references/DEPLOYED_TOKENS_BRIDGES_LPS_AND_ROUTING_STATUS.md` §4.1
+- cW bridge **pattern only**: `docs/07-ccip/CW_BRIDGE_APPROACH.md`
+- GRU policy **orthogonality**: `docs/04-configuration/GRU_REFERENCE_PRIMACY_AND_MESH_EXECUTION_MODEL.md`
diff --git a/contracts/wrapped-lp-public/WLPNAVOracle.sol b/contracts/wrapped-lp-public/WLPNAVOracle.sol
new file mode 100644
index 0000000..9020014
--- /dev/null
+++ b/contracts/wrapped-lp-public/WLPNAVOracle.sol
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/access/AccessControl.sol";
+
+/**
+ * @title WLPNAVOracle
+ * @notice Chainlink-style read interface for wLP / vault share price in USD (8 decimals) for lending adapters.
+ * @dev Keeper updates `latestAnswer` + `updatedAt`. Consumers must check `isStale()` before liquidations.
+ */
+contract WLPNAVOracle is AccessControl {
+ bytes32 public constant KEEPER_ROLE = keccak256("KEEPER_ROLE");
+
+ /// @notice Price with 8 decimals (Chainlink USD convention).
+ int256 public latestAnswer;
+ uint256 public updatedAt;
+ uint256 public heartbeat;
+ bool public circuitBreaker;
+
+ event AnswerUpdated(int256 indexed answer, uint256 updatedAt, address indexed keeper);
+ event HeartbeatUpdated(uint256 heartbeat);
+ event CircuitBreakerSet(bool tripped);
+
+ constructor(address admin, uint256 heartbeat_) {
+ require(admin != address(0), "NAV: zero admin");
+ heartbeat = heartbeat_;
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ }
+
+ function setHeartbeat(uint256 h) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ heartbeat = h;
+ emit HeartbeatUpdated(h);
+ }
+
+ function setCircuitBreaker(bool on) external onlyRole(DEFAULT_ADMIN_ROLE) {
+ circuitBreaker = on;
+ emit CircuitBreakerSet(on);
+ }
+
+ function submitAnswer(int256 answer) external onlyRole(KEEPER_ROLE) {
+ require(!circuitBreaker, "NAV: breaker");
+ latestAnswer = answer;
+ updatedAt = block.timestamp;
+ emit AnswerUpdated(answer, updatedAt, msg.sender);
+ }
+
+ function isStale() external view returns (bool) {
+ if (circuitBreaker) return true;
+ if (heartbeat == 0) return false;
+ return block.timestamp > updatedAt + heartbeat;
+ }
+}
diff --git a/contracts/wrapped-lp-public/WLPReceiptToken.sol b/contracts/wrapped-lp-public/WLPReceiptToken.sol
new file mode 100644
index 0000000..00a8ab9
--- /dev/null
+++ b/contracts/wrapped-lp-public/WLPReceiptToken.sol
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+import "@openzeppelin/contracts/access/AccessControl.sol";
+
+/**
+ * @title WLPReceiptToken
+ * @notice Fungible receipt token for Chain 138 LP exposure minted on a public chain (Option A).
+ * @dev Only addresses with MINTER_ROLE may mint; BURNER_ROLE for redemption gateway / bridge burn paths.
+ * Not a cW* token; separate from CompliantWrappedToken governance surface.
+ */
+contract WLPReceiptToken is ERC20, AccessControl {
+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
+ bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
+
+ uint8 private immutable _decimalsOverride;
+
+ constructor(
+ string memory name_,
+ string memory symbol_,
+ uint8 decimals_,
+ address admin
+ ) ERC20(name_, symbol_) {
+ require(admin != address(0), "WLP: zero admin");
+ _decimalsOverride = decimals_;
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ }
+
+ function decimals() public view override returns (uint8) {
+ return _decimalsOverride;
+ }
+
+ function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) {
+ _mint(to, amount);
+ }
+
+ function burn(address from, uint256 amount) external onlyRole(BURNER_ROLE) {
+ _burn(from, amount);
+ }
+}
diff --git a/contracts/wrapped-lp-public/WLPRedemptionGateway.sol b/contracts/wrapped-lp-public/WLPRedemptionGateway.sol
new file mode 100644
index 0000000..dfb2780
--- /dev/null
+++ b/contracts/wrapped-lp-public/WLPRedemptionGateway.sol
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/access/AccessControl.sol";
+import "./WLPReceiptToken.sol";
+import "./interfaces/IWLPProgramEvents.sol";
+
+/**
+ * @title WLPRedemptionGateway
+ * @notice Burns wLP from user and emits `RedemptionRequested` for relayers to release LP on Chain 138.
+ * @dev Relayer observes event, submits `Chain138LPLocker.release*` on 138. Fungible wLP + secondary trading
+ * may break 1:1 depositor-only redemption; production may require NFT receipts or curated policy.
+ */
+contract WLPRedemptionGateway is AccessControl, IWLPProgramEvents {
+ bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
+
+ WLPReceiptToken public immutable wlp;
+
+ constructor(address wlp_, address admin) {
+ require(wlp_ != address(0) && admin != address(0), "Gateway: zero");
+ wlp = WLPReceiptToken(wlp_);
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ }
+
+ /**
+ * @notice User burns wLP; relayer fulfills LP release on 138 per operational policy.
+ */
+ function requestRedeem(uint256 wlpAmount) external {
+ require(wlpAmount > 0, "Gateway: zero");
+ wlp.burn(msg.sender, wlpAmount);
+ emit RedemptionRequested(msg.sender, wlpAmount);
+ }
+}
diff --git a/contracts/wrapped-lp-public/WrappedLPNAVVault.sol b/contracts/wrapped-lp-public/WrappedLPNAVVault.sol
new file mode 100644
index 0000000..615b68f
--- /dev/null
+++ b/contracts/wrapped-lp-public/WrappedLPNAVVault.sol
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
+import "@openzeppelin/contracts/access/AccessControl.sol";
+
+/**
+ * @title WrappedLPNAVVault
+ * @notice Option B scaffold: standard ERC-4626 vault over an underlying asset (e.g. USDC on mainnet).
+ * @dev Strategy deployment of vault assets into Chain 138 LP is **operational** (outside this contract).
+ * Use `depositCap` for soft-launch; seed initial deposit to mitigate donation attacks per OZ ERC4626 docs.
+ */
+contract WrappedLPNAVVault is ERC4626, AccessControl {
+ bytes32 public constant CAP_ADMIN_ROLE = keccak256("CAP_ADMIN_ROLE");
+
+ uint256 public depositCap;
+
+ constructor(
+ IERC20 asset_,
+ string memory name_,
+ string memory symbol_,
+ address admin
+ ) ERC20(name_, symbol_) ERC4626(asset_) {
+ require(admin != address(0), "Vault: zero admin");
+ _grantRole(DEFAULT_ADMIN_ROLE, admin);
+ _grantRole(CAP_ADMIN_ROLE, admin);
+ }
+
+ function maxDeposit(address) public view override returns (uint256) {
+ uint256 cap = depositCap;
+ if (cap == 0) return type(uint256).max;
+ uint256 a = totalAssets();
+ if (a >= cap) return 0;
+ return cap - a;
+ }
+
+ function setDepositCap(uint256 cap) external onlyRole(CAP_ADMIN_ROLE) {
+ depositCap = cap;
+ }
+}
diff --git a/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol b/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol
new file mode 100644
index 0000000..39fce71
--- /dev/null
+++ b/contracts/wrapped-lp-public/interfaces/IWLPProgramEvents.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+/**
+ * @title IWLPProgramEvents
+ * @notice Canonical events for wrapped-LP / bridge attestation flows (Option A).
+ * @dev `lockRef` is the idempotency key for destination mint (replay protection).
+ */
+interface IWLPProgramEvents {
+ event LPLocked(bytes32 indexed lockRef, address indexed depositor, uint256 amount, address indexed lpToken);
+ event LPReleased(bytes32 indexed lockRef, address indexed to, uint256 amount, address indexed lpToken);
+ event WLPMinted(bytes32 indexed lockRef, address indexed recipient, uint256 amount);
+ event RedemptionRequested(address indexed holder, uint256 wlpAmount);
+}
diff --git a/deployments/chain138/sushiswap-native.json b/deployments/chain138/sushiswap-native.json
new file mode 100644
index 0000000..e138353
--- /dev/null
+++ b/deployments/chain138/sushiswap-native.json
@@ -0,0 +1,18 @@
+{
+ "chainId": 138,
+ "deployer": "0x4A666F96fC8764181194447A7dFdb7d471b301C8",
+ "feeToSetter": "0x4A666F96fC8764181194447A7dFdb7d471b301C8",
+ "deployedAtBlock": 4041495,
+ "factory": "0x2871207ff0d56089D70c0134d33f1291B6Fce0BE",
+ "router": "0xB37b93D38559f53b62ab020A14919f2630a1aE34",
+ "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "usdt": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1",
+ "usdc": "0x71D6687F38b93CCad569Fa6352c876eea967201b",
+ "cusdt": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
+ "cusdc": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
+ "pairs": {
+ "wethUsdt": "0x8d848941ee8Daca6fc22A9B8a11Ed706Ad60A80D",
+ "wethUsdc": "0xaf000a8ea892502B4d3eB7BD0127268468339DE7",
+ "cusdtCusdc": "0x43D93d520079DCB6Fe97Abf6e7607129893b0e3E"
+ }
+}
diff --git a/deployments/chain138/uniswap-v2-native.json b/deployments/chain138/uniswap-v2-native.json
new file mode 100644
index 0000000..4c08481
--- /dev/null
+++ b/deployments/chain138/uniswap-v2-native.json
@@ -0,0 +1,17 @@
+{
+ "chainId": 138,
+ "deployer": "0x4A666F96fC8764181194447A7dFdb7d471b301C8",
+ "deployedAtBlock": 4041469,
+ "factory": "0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279",
+ "router": "0x3019A7fDc76ba7F64F18d78e66842760037ee638",
+ "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ "usdt": "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1",
+ "usdc": "0x71D6687F38b93CCad569Fa6352c876eea967201b",
+ "cusdt": "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
+ "cusdc": "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
+ "pairs": {
+ "wethUsdt": "0xda2A5AC9F4892EAd47093A374779Ba790f23DC84",
+ "wethUsdc": "0xCD255F4f6c4E861490b3C7E0300613318B870f36",
+ "cusdtCusdc": "0x7bd707294AACb47ab109780be8a096700741886d"
+ }
+}
diff --git a/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md b/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md
index 4707e5f..9a671f0 100644
--- a/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md
+++ b/docs/integration/ORACLE_AND_KEEPER_CHAIN138.md
@@ -49,6 +49,8 @@ References: [DEPLOYMENT_COMPLETE_GUIDE.md](../deployment/DEPLOYMENT_COMPLETE_GUI
**Policy:** Run off-chain ticks every **6 seconds** so oracle pushes, `PriceFeedKeeper` upkeep, and PMM/WETH read paths stay warm—supporting peg alignment with `DODOPMMIntegration` / optional `ReserveSystem`.
+**One-shot on-chain alignment (WETH10 mock + D3Oracle + `trackAsset`):** parent repo `scripts/deployment/fix-chain138-pricing-feeds.sh` (see `docs/04-configuration/CHAIN138_PRICING_FEEDS_LIVE.md`).
+
| Component | Role |
|-----------|------|
| `scripts/reserve/pmm-mesh-6s-automation.sh` | Loop: `getPoolPriceOrOracle` on `PMM_MESH_POLL_POOLS`, WETH9/WETH10 `totalSupply` reads, conditional `performUpkeep`, `update-oracle-price.sh`. |
diff --git a/hardhat.config.js b/hardhat.config.js
index 699406d..5d1c250 100644
--- a/hardhat.config.js
+++ b/hardhat.config.js
@@ -28,6 +28,9 @@ const sharedAccounts = resolveAccounts();
module.exports = {
solidity: {
compilers: [
+ { version: "0.5.16", settings: { optimizer: { enabled: true, runs: 200 } } },
+ { version: "0.6.6", settings: { optimizer: { enabled: true, runs: 200 } } },
+ { version: "0.6.12", settings: { optimizer: { enabled: true, runs: 200 } } },
{ version: "0.8.20", settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true } },
{ version: "0.8.22", settings: { optimizer: { enabled: true, runs: 200 }, viaIR: true } },
],
diff --git a/lib/dodo-contractV2 b/lib/dodo-contractV2
index d946606..2e0a364 160000
--- a/lib/dodo-contractV2
+++ b/lib/dodo-contractV2
@@ -1 +1 @@
-Subproject commit d946606870b64110218820da44becf2b3e196c8a
+Subproject commit 2e0a36474323b5803d71e6de8e9336b2d350a10e
diff --git a/script/deploy/chains/DeployAllAdapters.s.sol b/script/deploy/chains/DeployAllAdapters.s.sol
index 345dde4..843c865 100644
--- a/script/deploy/chains/DeployAllAdapters.s.sol
+++ b/script/deploy/chains/DeployAllAdapters.s.sol
@@ -8,7 +8,9 @@ import "../../../contracts/bridge/adapters/evm/XDCAdapter.sol";
import "../../../contracts/bridge/adapters/evm/AlltraAdapter.sol";
import "../../../contracts/bridge/adapters/non-evm/XRPLAdapter.sol";
import "../../../contracts/bridge/adapters/non-evm/StellarAdapter.sol";
+import "../../../contracts/bridge/adapters/non-evm/SolanaAdapter.sol";
import "../../../contracts/bridge/adapters/non-evm/TezosAdapter.sol";
+import "../../../contracts/bridge/adapters/non-evm/TronAdapter.sol";
import "../../../contracts/bridge/adapters/hyperledger/FireflyAdapter.sol";
import "../../../contracts/bridge/adapters/hyperledger/CactiAdapter.sol";
import "../../../contracts/bridge/adapters/hyperledger/FabricAdapter.sol";
@@ -39,7 +41,9 @@ contract DeployAllAdapters is Script {
// Deploy non-EVM adapters
XRPLAdapter xrplAdapter = new XRPLAdapter(deployer);
StellarAdapter stellarAdapter = new StellarAdapter(deployer);
+ SolanaAdapter solanaAdapter = new SolanaAdapter(deployer);
TezosAdapter tezosAdapter = new TezosAdapter(deployer);
+ TronAdapter tronAdapter = new TronAdapter(deployer);
// Deploy Hyperledger adapters
FireflyAdapter fireflyAdapter = new FireflyAdapter(deployer, "alltra-bridge");
@@ -59,7 +63,9 @@ contract DeployAllAdapters is Script {
registry.registerNonEVMChain("XRPL-Mainnet", ChainRegistry.ChainType.XRPL, address(xrplAdapter), "https://xrpscan.com", 1, 4, true, "");
registry.registerNonEVMChain("Stellar-Mainnet", ChainRegistry.ChainType.Stellar, address(stellarAdapter), "https://stellarchain.io", 1, 5, true, "");
+ registry.registerNonEVMChain("Solana-Mainnet", ChainRegistry.ChainType.Solana, address(solanaAdapter), "https://solscan.io", 32, 1, true, "");
registry.registerNonEVMChain("Tezos-Mainnet", ChainRegistry.ChainType.Other, address(tezosAdapter), "https://tzkt.io", 1, 30, true, "");
+ registry.registerNonEVMChain("Tron-Mainnet", ChainRegistry.ChainType.Tron, address(tronAdapter), "https://tronscan.org", 20, 3, true, "");
registry.registerNonEVMChain("Firefly-Orchestration", ChainRegistry.ChainType.Firefly, address(fireflyAdapter), "", 1, 1, true, "");
registry.registerNonEVMChain("Cacti-Interoperability", ChainRegistry.ChainType.Cacti, address(cactiAdapter), "", 1, 1, true, "");
registry.registerNonEVMChain("Fabric-bridge-channel", ChainRegistry.ChainType.Fabric, address(fabricAdapter), "", 1, 1, true, "");
diff --git a/script/deploy/chains/DeploySolanaAdapter.s.sol b/script/deploy/chains/DeploySolanaAdapter.s.sol
new file mode 100644
index 0000000..6119d84
--- /dev/null
+++ b/script/deploy/chains/DeploySolanaAdapter.s.sol
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import "forge-std/Script.sol";
+import "../../../contracts/registry/ChainRegistry.sol";
+import "../../../contracts/bridge/adapters/non-evm/SolanaAdapter.sol";
+
+contract DeploySolanaAdapter is Script {
+ function run() external {
+ uint256 pk = vm.envUint("PRIVATE_KEY");
+ address admin = vm.addr(pk);
+ address chainRegistry = vm.envAddress("CHAIN_REGISTRY_ADDRESS");
+
+ ChainRegistry registry = ChainRegistry(chainRegistry);
+
+ vm.startBroadcast(pk);
+
+ SolanaAdapter solanaAdapter = new SolanaAdapter(admin);
+ ChainRegistry.ChainMetadata memory existing = registry.getNonEVMChain("Solana-Mainnet");
+
+ if (existing.adapter == address(0)) {
+ registry.registerNonEVMChain(
+ "Solana-Mainnet",
+ ChainRegistry.ChainType.Solana,
+ address(solanaAdapter),
+ "https://solscan.io",
+ 32,
+ 1,
+ true,
+ ""
+ );
+ } else {
+ registry.updateAdapter(0, "Solana-Mainnet", address(solanaAdapter));
+ registry.setChainActive(0, "Solana-Mainnet", true);
+ }
+
+ vm.stopBroadcast();
+
+ console.log("SolanaAdapter:", address(solanaAdapter));
+ console.log("ChainRegistry:", chainRegistry);
+ }
+}
diff --git a/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol b/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol
new file mode 100644
index 0000000..7b42db1
--- /dev/null
+++ b/script/wrapped-lp-public/DeployWrappedLPLockerChain138.s.sol
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Script, console2} from "forge-std/Script.sol";
+
+import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol";
+
+/**
+ * @title DeployWrappedLPLockerChain138
+ * @notice Deploy **only** `Chain138LPLocker` on Chain 138. Use `--rpc-url` pointing at Core RPC.
+ * @dev Env: `LP_TOKEN`, `ADMIN`, `PRIVATE_KEY`. Verify on Blockscout after deploy.
+ */
+contract DeployWrappedLPLockerChain138 is Script {
+ function run() external {
+ address lpToken = vm.envAddress("LP_TOKEN");
+ address admin = vm.envAddress("ADMIN");
+ uint256 pk = vm.envUint("PRIVATE_KEY");
+
+ vm.startBroadcast(pk);
+
+ Chain138LPLocker locker = new Chain138LPLocker(lpToken, admin);
+ console2.log("Chain138LPLocker", address(locker));
+
+ vm.stopBroadcast();
+ }
+}
diff --git a/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol b/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol
new file mode 100644
index 0000000..7b3b474
--- /dev/null
+++ b/script/wrapped-lp-public/DeployWrappedLPProgram.s.sol
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Script, console2} from "forge-std/Script.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol";
+import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol";
+import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol";
+import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol";
+import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol";
+import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol";
+
+/**
+ * @title DeployWrappedLPProgram
+ * @notice **Single-chain / local harness:** deploys locker + public stack in one `run()` (only valid when
+ * both sides target the **same** RPC, e.g. Anvil or fork). For production use:
+ * - [DeployWrappedLPLockerChain138.s.sol](DeployWrappedLPLockerChain138.s.sol) with Chain 138 RPC
+ * - [DeployWrappedLPPublicChain.s.sol](DeployWrappedLPPublicChain.s.sol) with destination RPC
+ * @dev Env: `LP_TOKEN`, `ADMIN`, `USDC` (optional vault), `PRIVATE_KEY`. Optional `WIRE_ROLES=1` and
+ * `RELAYER_ADDRESS` when `vm.addr(PRIVATE_KEY) == ADMIN`.
+ */
+contract DeployWrappedLPProgram is Script {
+ function run() external {
+ address lpToken = vm.envAddress("LP_TOKEN");
+ address admin = vm.envAddress("ADMIN");
+ uint256 pk = vm.envUint("PRIVATE_KEY");
+
+ vm.startBroadcast(pk);
+
+ Chain138LPLocker locker = new Chain138LPLocker(lpToken, admin);
+ console2.log("Chain138LPLocker", address(locker));
+
+ WLPReceiptToken wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin);
+ console2.log("WLPReceiptToken", address(wlp));
+
+ PublicChainMintController mintCtl = new PublicChainMintController(address(wlp), address(locker), admin);
+ console2.log("PublicChainMintController", address(mintCtl));
+
+ WLPRedemptionGateway gateway = new WLPRedemptionGateway(address(wlp), admin);
+ console2.log("WLPRedemptionGateway", address(gateway));
+
+ WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600);
+ console2.log("WLPNAVOracle", address(oracle));
+
+ address usdc = vm.envOr("USDC", address(0));
+ if (usdc != address(0)) {
+ WrappedLPNAVVault vault = new WrappedLPNAVVault(
+ IERC20(usdc),
+ "Wrapped LP NAV Vault",
+ "wLPV",
+ admin
+ );
+ console2.log("WrappedLPNAVVault", address(vault));
+ }
+
+ if (vm.envOr("WIRE_ROLES", false)) {
+ address deployer = vm.addr(pk);
+ require(deployer == admin, "WIRE_ROLES requires deployer==ADMIN");
+ address relayerAddr = vm.envAddress("RELAYER_ADDRESS");
+ address keeperAddr = vm.envOr("KEEPER_ADDRESS", relayerAddr);
+ wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl));
+ wlp.grantRole(wlp.BURNER_ROLE(), address(gateway));
+ mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayerAddr);
+ locker.grantRole(locker.BRIDGE_RELEASE_ROLE(), relayerAddr);
+ oracle.grantRole(oracle.KEEPER_ROLE(), keeperAddr);
+ console2.log("Wired roles (minter/burner/relayer/release/keeper)");
+ }
+
+ vm.stopBroadcast();
+ }
+}
diff --git a/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol b/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol
new file mode 100644
index 0000000..1a2ef04
--- /dev/null
+++ b/script/wrapped-lp-public/DeployWrappedLPPublicChain.s.sol
@@ -0,0 +1,65 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Script, console2} from "forge-std/Script.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+
+import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol";
+import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol";
+import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol";
+import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol";
+import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol";
+
+/**
+ * @title DeployWrappedLPPublicChain
+ * @notice Deploy wLP stack on **public** chain (mainnet or L2). `--rpc-url` must be destination chain.
+ * @dev Env: `ADMIN`, `PRIVATE_KEY`, `CHAIN138_LOCKER` (locker address for mint controller reference),
+ * optional `USDC` for vault, optional `WIRE_ROLES=1`, `RELAYER_ADDRESS`, `KEEPER_ADDRESS` (defaults to relayer).
+ */
+contract DeployWrappedLPPublicChain is Script {
+ function run() external {
+ address admin = vm.envAddress("ADMIN");
+ address chain138Locker = vm.envAddress("CHAIN138_LOCKER");
+ uint256 pk = vm.envUint("PRIVATE_KEY");
+
+ vm.startBroadcast(pk);
+
+ WLPReceiptToken wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin);
+ console2.log("WLPReceiptToken", address(wlp));
+
+ PublicChainMintController mintCtl = new PublicChainMintController(address(wlp), chain138Locker, admin);
+ console2.log("PublicChainMintController", address(mintCtl));
+
+ WLPRedemptionGateway gateway = new WLPRedemptionGateway(address(wlp), admin);
+ console2.log("WLPRedemptionGateway", address(gateway));
+
+ WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600);
+ console2.log("WLPNAVOracle", address(oracle));
+
+ address usdc = vm.envOr("USDC", address(0));
+ if (usdc != address(0)) {
+ WrappedLPNAVVault vault = new WrappedLPNAVVault(
+ IERC20(usdc),
+ "Wrapped LP NAV Vault",
+ "wLPV",
+ admin
+ );
+ console2.log("WrappedLPNAVVault", address(vault));
+ }
+
+ if (vm.envOr("WIRE_ROLES", false)) {
+ address deployer = vm.addr(pk);
+ require(deployer == admin, "WIRE_ROLES requires deployer==ADMIN");
+ address relayerAddr = vm.envAddress("RELAYER_ADDRESS");
+ address keeperAddr = vm.envOr("KEEPER_ADDRESS", relayerAddr);
+
+ wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl));
+ wlp.grantRole(wlp.BURNER_ROLE(), address(gateway));
+ mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayerAddr);
+ oracle.grantRole(oracle.KEEPER_ROLE(), keeperAddr);
+ console2.log("Wired MINTER/BURNER/RELAYER/KEEPER");
+ }
+
+ vm.stopBroadcast();
+ }
+}
diff --git a/scripts/chain138/deploy-sushiswap-native.js b/scripts/chain138/deploy-sushiswap-native.js
new file mode 100644
index 0000000..07df648
--- /dev/null
+++ b/scripts/chain138/deploy-sushiswap-native.js
@@ -0,0 +1,188 @@
+const fs = require("fs");
+const path = require("path");
+const hre = require("hardhat");
+
+const DEFAULTS = {
+ weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ usdt: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1",
+ usdc: "0x71D6687F38b93CCad569Fa6352c876eea967201b",
+ cusdt: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
+ cusdc: "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
+};
+
+function envAddress(name, fallback) {
+ const value = String(process.env[name] || fallback || "").trim();
+ if (!value) {
+ throw new Error(`Missing required address env ${name}`);
+ }
+ return value;
+}
+
+function envUnits(name, fallback, decimals) {
+ return hre.ethers.parseUnits(String(process.env[name] || fallback), decimals);
+}
+
+async function ensureAllowance(token, owner, spender, amount) {
+ const allowance = await token.allowance(owner, spender);
+ if (allowance >= amount) return;
+ const tx = await token.approve(spender, hre.ethers.MaxUint256);
+ await tx.wait();
+}
+
+async function transferIfNeeded(token, to, amount) {
+ if (amount <= 0n) return;
+ const tx = await token.transfer(to, amount);
+ await tx.wait();
+}
+
+async function main() {
+ const signers = await hre.ethers.getSigners();
+ const deployer = signers[0];
+ const feeTo = signers[5] || deployer;
+ const currentBlock = await hre.ethers.provider.getBlockNumber();
+ const gasPrice = BigInt(process.env.CHAIN138_SUSHISWAP_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", [])));
+ const deployOverrides = {
+ type: 0,
+ gasPrice,
+ gasLimit: BigInt(process.env.CHAIN138_SUSHISWAP_DEPLOY_GAS_LIMIT || "9000000"),
+ };
+ const liquidityOverrides = {
+ type: 0,
+ gasPrice,
+ gasLimit: BigInt(process.env.CHAIN138_SUSHISWAP_LIQUIDITY_GAS_LIMIT || "3000000"),
+ };
+
+ const weth = envAddress("CHAIN138_NATIVE_WETH9", process.env.WETH9 || DEFAULTS.weth);
+ const usdt = envAddress("CHAIN138_NATIVE_USDT", process.env.OFFICIAL_USDT_ADDRESS || DEFAULTS.usdt);
+ const usdc = envAddress("CHAIN138_NATIVE_USDC", process.env.OFFICIAL_USDC_ADDRESS || DEFAULTS.usdc);
+ const cusdt = envAddress("CHAIN138_COMPLIANT_USDT", process.env.COMPLIANT_USDT_ADDRESS || DEFAULTS.cusdt);
+ const cusdc = envAddress("CHAIN138_COMPLIANT_USDC", process.env.COMPLIANT_USDC_ADDRESS || DEFAULTS.cusdc);
+
+ const Factory = await hre.ethers.getContractFactory(
+ "contracts/vendor/sushiswap-v2/UniswapV2Factory.sol:UniswapV2Factory"
+ );
+ const Router = await hre.ethers.getContractFactory(
+ "contracts/vendor/sushiswap-v2/UniswapV2Router02.sol:UniswapV2Router02"
+ );
+ const Pair = await hre.ethers.getContractFactory(
+ "contracts/vendor/sushiswap-v2/UniswapV2Pair.sol:UniswapV2Pair"
+ );
+
+ let factory;
+ let router;
+
+ const existingFactory = process.env.CHAIN138_SUSHISWAP_EXISTING_FACTORY?.trim();
+ const existingRouter = process.env.CHAIN138_SUSHISWAP_EXISTING_ROUTER?.trim();
+ if (existingFactory && existingRouter) {
+ factory = Factory.attach(existingFactory);
+ router = Router.attach(existingRouter);
+ console.log(`[reuse] factory=${existingFactory}`);
+ console.log(`[reuse] router=${existingRouter}`);
+ } else {
+ console.log(`[deploy] SushiSwapFactory gasPrice=${gasPrice}`);
+ factory = await Factory.deploy(feeTo.address, deployOverrides);
+ await factory.waitForDeployment();
+ console.log(`[ok] factory=${await factory.getAddress()}`);
+ console.log(`[deploy] SushiSwapRouter02`);
+ router = await Router.deploy(await factory.getAddress(), weth, deployOverrides);
+ await router.waitForDeployment();
+ console.log(`[ok] router=${await router.getAddress()}`);
+ }
+
+ const erc20Abi = [
+ "function transfer(address to,uint256 amount) returns (bool)",
+ "function approve(address spender,uint256 amount) returns (bool)",
+ "function allowance(address owner,address spender) view returns (uint256)",
+ "function balanceOf(address account) view returns (uint256)",
+ ];
+ const tokenByAddress = {
+ [weth.toLowerCase()]: new hre.ethers.Contract(weth, erc20Abi, deployer),
+ [usdt.toLowerCase()]: new hre.ethers.Contract(usdt, erc20Abi, deployer),
+ [usdc.toLowerCase()]: new hre.ethers.Contract(usdc, erc20Abi, deployer),
+ [cusdt.toLowerCase()]: new hre.ethers.Contract(cusdt, erc20Abi, deployer),
+ [cusdc.toLowerCase()]: new hre.ethers.Contract(cusdc, erc20Abi, deployer),
+ };
+
+ const seedSpecs = [
+ {
+ key: "wethUsdt",
+ tokenA: weth,
+ tokenB: usdt,
+ amountA: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDT_WETH", "25", 18),
+ amountB: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDT_STABLE", "52915", 6),
+ },
+ {
+ key: "wethUsdc",
+ tokenA: weth,
+ tokenB: usdc,
+ amountA: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDC_WETH", "25", 18),
+ amountB: envUnits("CHAIN138_SUSHISWAP_SEED_WETH_USDC_STABLE", "52915", 6),
+ },
+ {
+ key: "cusdtCusdc",
+ tokenA: cusdt,
+ tokenB: cusdc,
+ amountA: envUnits("CHAIN138_SUSHISWAP_SEED_CUSDT_CUSDC_CUSDT", "250000", 6),
+ amountB: envUnits("CHAIN138_SUSHISWAP_SEED_CUSDT_CUSDC_CUSDC", "250000", 6),
+ },
+ ];
+
+ const deployedPairs = {};
+
+ for (const spec of seedSpecs) {
+ console.log(`[seed] ${spec.key}`);
+ let pairAddress = await factory.getPair(spec.tokenA, spec.tokenB);
+ if (pairAddress === hre.ethers.ZeroAddress) {
+ const tx = await factory.createPair(spec.tokenA, spec.tokenB, liquidityOverrides);
+ await tx.wait();
+ pairAddress = await factory.getPair(spec.tokenA, spec.tokenB);
+ }
+
+ const pair = Pair.attach(pairAddress);
+ const [reserve0, reserve1] = await pair.getReserves();
+ if (reserve0 > 0n || reserve1 > 0n) {
+ deployedPairs[spec.key] = pairAddress;
+ console.log(`[skip] ${spec.key} already seeded ${pairAddress}`);
+ continue;
+ }
+
+ const currentABalance = await tokenByAddress[spec.tokenA.toLowerCase()].balanceOf(pairAddress);
+ const currentBBalance = await tokenByAddress[spec.tokenB.toLowerCase()].balanceOf(pairAddress);
+ const topUpA = spec.amountA > currentABalance ? spec.amountA - currentABalance : 0n;
+ const topUpB = spec.amountB > currentBBalance ? spec.amountB - currentBBalance : 0n;
+
+ await transferIfNeeded(tokenByAddress[spec.tokenA.toLowerCase()], pairAddress, topUpA);
+ await transferIfNeeded(tokenByAddress[spec.tokenB.toLowerCase()], pairAddress, topUpB);
+
+ const mintTx = await pair.mint(deployer.address, liquidityOverrides);
+ await mintTx.wait();
+ deployedPairs[spec.key] = pairAddress;
+ console.log(`[ok] ${spec.key}=${deployedPairs[spec.key]}`);
+ }
+
+ const output = {
+ chainId: 138,
+ deployer: deployer.address,
+ feeToSetter: feeTo.address,
+ deployedAtBlock: currentBlock,
+ factory: await factory.getAddress(),
+ router: await router.getAddress(),
+ weth,
+ usdt,
+ usdc,
+ cusdt,
+ cusdc,
+ pairs: deployedPairs,
+ };
+
+ const outputPath = path.resolve(__dirname, "../../deployments/chain138/sushiswap-native.json");
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + "\n");
+
+ console.log(JSON.stringify(output, null, 2));
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/scripts/chain138/deploy-uniswap-v2-native.js b/scripts/chain138/deploy-uniswap-v2-native.js
new file mode 100644
index 0000000..6db621f
--- /dev/null
+++ b/scripts/chain138/deploy-uniswap-v2-native.js
@@ -0,0 +1,186 @@
+const fs = require("fs");
+const path = require("path");
+const hre = require("hardhat");
+
+const DEFAULTS = {
+ weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
+ usdt: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1",
+ usdc: "0x71D6687F38b93CCad569Fa6352c876eea967201b",
+ cusdt: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22",
+ cusdc: "0xf22258f57794CC8E06237084b353Ab30fFfa640b",
+};
+
+function envAddress(name, fallback) {
+ const value = String(process.env[name] || fallback || "").trim();
+ if (!value) {
+ throw new Error(`Missing required address env ${name}`);
+ }
+ return value;
+}
+
+function envUnits(name, fallback, decimals) {
+ return hre.ethers.parseUnits(String(process.env[name] || fallback), decimals);
+}
+
+async function ensureAllowance(token, owner, spender, amount) {
+ const allowance = await token.allowance(owner, spender);
+ if (allowance >= amount) return;
+ const tx = await token.approve(spender, hre.ethers.MaxUint256);
+ await tx.wait();
+}
+
+async function transferIfNeeded(token, to, amount) {
+ if (amount <= 0n) return;
+ const tx = await token.transfer(to, amount);
+ await tx.wait();
+}
+
+async function main() {
+ const [deployer] = await hre.ethers.getSigners();
+ const currentBlock = await hre.ethers.provider.getBlockNumber();
+ const gasPrice = BigInt(process.env.CHAIN138_NATIVE_V2_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", [])));
+ const deployOverrides = {
+ type: 0,
+ gasPrice,
+ gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_DEPLOY_GAS_LIMIT || "9000000"),
+ };
+ const liquidityOverrides = {
+ type: 0,
+ gasPrice,
+ gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_LIQUIDITY_GAS_LIMIT || "3000000"),
+ };
+
+ const weth = envAddress("CHAIN138_NATIVE_WETH9", process.env.WETH9 || DEFAULTS.weth);
+ const usdt = envAddress("CHAIN138_NATIVE_USDT", process.env.OFFICIAL_USDT_ADDRESS || DEFAULTS.usdt);
+ const usdc = envAddress("CHAIN138_NATIVE_USDC", process.env.OFFICIAL_USDC_ADDRESS || DEFAULTS.usdc);
+ const cusdt = envAddress("CHAIN138_COMPLIANT_USDT", process.env.COMPLIANT_USDT_ADDRESS || DEFAULTS.cusdt);
+ const cusdc = envAddress("CHAIN138_COMPLIANT_USDC", process.env.COMPLIANT_USDC_ADDRESS || DEFAULTS.cusdc);
+
+ const Factory = await hre.ethers.getContractFactory(
+ "contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol:UniswapV2Factory"
+ );
+ const Router = await hre.ethers.getContractFactory(
+ "contracts/vendor/uniswap-v2-periphery/UniswapV2Router02.sol:UniswapV2Router02"
+ );
+ const Pair = await hre.ethers.getContractFactory(
+ "contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol:UniswapV2Pair"
+ );
+
+ let factory;
+ let router;
+
+ const existingFactory = process.env.CHAIN138_UNISWAP_V2_EXISTING_FACTORY?.trim();
+ const existingRouter = process.env.CHAIN138_UNISWAP_V2_EXISTING_ROUTER?.trim();
+ if (existingFactory && existingRouter) {
+ factory = Factory.attach(existingFactory);
+ router = Router.attach(existingRouter);
+ console.log(`[reuse] factory=${existingFactory}`);
+ console.log(`[reuse] router=${existingRouter}`);
+ } else {
+ console.log(`[deploy] UniswapV2Factory gasPrice=${gasPrice}`);
+ factory = await Factory.deploy(deployer.address, deployOverrides);
+ await factory.waitForDeployment();
+ console.log(`[ok] factory=${await factory.getAddress()}`);
+ console.log(`[deploy] UniswapV2Router02`);
+ router = await Router.deploy(await factory.getAddress(), weth, deployOverrides);
+ await router.waitForDeployment();
+ console.log(`[ok] router=${await router.getAddress()}`);
+ }
+
+ const erc20Abi = [
+ "function transfer(address to,uint256 amount) returns (bool)",
+ "function approve(address spender,uint256 amount) returns (bool)",
+ "function allowance(address owner,address spender) view returns (uint256)",
+ "function balanceOf(address account) view returns (uint256)",
+ ];
+ const tokenByAddress = {
+ [weth.toLowerCase()]: new hre.ethers.Contract(weth, erc20Abi, deployer),
+ [usdt.toLowerCase()]: new hre.ethers.Contract(usdt, erc20Abi, deployer),
+ [usdc.toLowerCase()]: new hre.ethers.Contract(usdc, erc20Abi, deployer),
+ [cusdt.toLowerCase()]: new hre.ethers.Contract(cusdt, erc20Abi, deployer),
+ [cusdc.toLowerCase()]: new hre.ethers.Contract(cusdc, erc20Abi, deployer),
+ };
+
+ const seedSpecs = [
+ {
+ key: "wethUsdt",
+ tokenA: weth,
+ tokenB: usdt,
+ amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDT_WETH", "25", 18),
+ amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDT_STABLE", "52915", 6),
+ },
+ {
+ key: "wethUsdc",
+ tokenA: weth,
+ tokenB: usdc,
+ amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDC_WETH", "25", 18),
+ amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_WETH_USDC_STABLE", "52915", 6),
+ },
+ {
+ key: "cusdtCusdc",
+ tokenA: cusdt,
+ tokenB: cusdc,
+ amountA: envUnits("CHAIN138_UNISWAP_V2_SEED_CUSDT_CUSDC_CUSDT", "250000", 6),
+ amountB: envUnits("CHAIN138_UNISWAP_V2_SEED_CUSDT_CUSDC_CUSDC", "250000", 6),
+ },
+ ];
+
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
+ const deployedPairs = {};
+
+ for (const spec of seedSpecs) {
+ console.log(`[seed] ${spec.key}`);
+ let pairAddress = await factory.getPair(spec.tokenA, spec.tokenB);
+ if (pairAddress === hre.ethers.ZeroAddress) {
+ const tx = await factory.createPair(spec.tokenA, spec.tokenB, liquidityOverrides);
+ await tx.wait();
+ pairAddress = await factory.getPair(spec.tokenA, spec.tokenB);
+ }
+
+ const pair = Pair.attach(pairAddress);
+ const [reserve0, reserve1] = await pair.getReserves();
+ if (reserve0 > 0n || reserve1 > 0n) {
+ deployedPairs[spec.key] = pairAddress;
+ console.log(`[skip] ${spec.key} already seeded ${pairAddress}`);
+ continue;
+ }
+
+ const currentABalance = await tokenByAddress[spec.tokenA.toLowerCase()].balanceOf(pairAddress);
+ const currentBBalance = await tokenByAddress[spec.tokenB.toLowerCase()].balanceOf(pairAddress);
+ const topUpA = spec.amountA > currentABalance ? spec.amountA - currentABalance : 0n;
+ const topUpB = spec.amountB > currentBBalance ? spec.amountB - currentBBalance : 0n;
+
+ await transferIfNeeded(tokenByAddress[spec.tokenA.toLowerCase()], pairAddress, topUpA);
+ await transferIfNeeded(tokenByAddress[spec.tokenB.toLowerCase()], pairAddress, topUpB);
+
+ const mintTx = await pair.mint(deployer.address, liquidityOverrides);
+ await mintTx.wait();
+ deployedPairs[spec.key] = pairAddress;
+ console.log(`[ok] ${spec.key}=${deployedPairs[spec.key]}`);
+ }
+
+ const output = {
+ chainId: 138,
+ deployer: deployer.address,
+ deployedAtBlock: currentBlock,
+ factory: await factory.getAddress(),
+ router: await router.getAddress(),
+ weth,
+ usdt,
+ usdc,
+ cusdt,
+ cusdc,
+ pairs: deployedPairs,
+ };
+
+ const outputPath = path.resolve(__dirname, "../../deployments/chain138/uniswap-v2-native.json");
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2) + "\n");
+
+ console.log(JSON.stringify(output, null, 2));
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/scripts/chain138/seed-uni-v2-weth-quote-pairs.js b/scripts/chain138/seed-uni-v2-weth-quote-pairs.js
new file mode 100644
index 0000000..47f9446
--- /dev/null
+++ b/scripts/chain138/seed-uni-v2-weth-quote-pairs.js
@@ -0,0 +1,274 @@
+/**
+ * Create and seed Uniswap V2 pairs on Chain 138: each canonical asset vs WETH9 (ERC-20) in the pair.
+ *
+ * Funding the ETH leg (no explicit wrap in this script):
+ * - Liquidity is added via UniswapV2Router02.addLiquidityETH: you send native ETH as `msg.value`.
+ * - The router wraps to WETH9 inside the call and mints LP; this script never calls WETH9.deposit().
+ * - Pair reserves are still WETH + token (AMM math cannot store raw ETH in the pair contract).
+ *
+ * Prerequisites:
+ * - PRIVATE_KEY; deployer holds native ETH (for `value` + gas) + tokens (or MINTER_ROLE to mint).
+ * - Factory + router: CHAIN138_UNISWAP_V2_EXISTING_* or deployments/chain138/uniswap-v2-native.json
+ *
+ * Usage (from smom-dbis-138):
+ * source scripts/load-env.sh
+ * npx hardhat run scripts/chain138/seed-uni-v2-weth-quote-pairs.js --network chain138
+ *
+ * Tuning:
+ * CHAIN138_NATIVE_WETH9 — must match router’s WETH (default 0xC02a…)
+ * CHAIN138_UNI_V2_DEFAULT_WETH — native ETH `value` per new pair (default 0.25)
+ * CHAIN138_UNI_V2_STABLE_PER_WETH — USD face (6-decimal) per 1 ETH for stables (default 2100)
+ * CHAIN138_UNI_V2_SLIPPAGE_BPS — min amounts for addLiquidityETH (default 50 = 0.5%)
+ * CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI — extra wei required on top of `value` (default ~0.03 ETH for gas)
+ * Per-pair: CHAIN138_UNI_V2_SEED__WETH and CHAIN138_UNI_V2_SEED__TOKEN
+ */
+const fs = require("fs");
+const path = require("path");
+const hre = require("hardhat");
+
+const WETH9 = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";
+
+/** Canonical Chain 138 tokens (see docs/11-references/EXPLORER_TOKEN_LIST_CROSSCHECK.md). */
+const WETH_QUOTE_TARGETS = [
+ { key: "wethLink", symbol: "LINK", address: "0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03", decimals: 18, kind: "link" },
+ { key: "wethCusdt", symbol: "cUSDT", address: "0x93E66202A11B1772E55407B32B44e5Cd8eda7f22", decimals: 6, kind: "stable6" },
+ { key: "wethCusdc", symbol: "cUSDC", address: "0xf22258f57794CC8E06237084b353Ab30fFfa640b", decimals: 6, kind: "stable6" },
+ { key: "wethWeth10", symbol: "WETH10", address: "0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f", decimals: 18, kind: "weth10" },
+ { key: "wethCeurc", symbol: "cEURC", address: "0x8085961F9cF02b4d800A3c6d386D31da4B34266a", decimals: 6, kind: "stable6" },
+ { key: "wethCeurt", symbol: "cEURT", address: "0xdf4b71c61E5912712C1Bdd451416B9aC26949d72", decimals: 6, kind: "stable6" },
+ { key: "wethCgbpc", symbol: "cGBPC", address: "0x003960f16D9d34F2e98d62723B6721Fb92074aD2", decimals: 6, kind: "stable6" },
+ { key: "wethCgbrt", symbol: "cGBPT", address: "0x350f54e4D23795f86A9c03988c7135357CCaD97c", decimals: 6, kind: "stable6" },
+ { key: "wethCaudc", symbol: "cAUDC", address: "0xD51482e567c03899eecE3CAe8a058161FD56069D", decimals: 6, kind: "stable6" },
+ { key: "wethCjpyc", symbol: "cJPYC", address: "0xEe269e1226a334182aace90056EE4ee5Cc8A6770", decimals: 6, kind: "stable6" },
+ { key: "wethCchfc", symbol: "cCHFC", address: "0x873990849DDa5117d7C644f0aF24370797C03885", decimals: 6, kind: "stable6" },
+ { key: "wethCcadc", symbol: "cCADC", address: "0x54dBd40cF05e15906A2C21f600937e96787f5679", decimals: 6, kind: "stable6" },
+ { key: "wethCxauc", symbol: "cXAUC", address: "0x290E52a8819A4fbD0714E517225429aA2B70EC6b", decimals: 6, kind: "xau6" },
+ { key: "wethCxaut", symbol: "cXAUT", address: "0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E", decimals: 6, kind: "xau6" },
+ { key: "wethUsdt", symbol: "USDT", address: "0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1", decimals: 6, kind: "stable6" },
+ { key: "wethUsdc", symbol: "USDC", address: "0x71D6687F38b93CCad569Fa6352c876eea967201b", decimals: 6, kind: "stable6" },
+];
+
+const erc20Abi = [
+ "function transfer(address to,uint256 amount) returns (bool)",
+ "function approve(address spender,uint256 amount) returns (bool)",
+ "function balanceOf(address account) view returns (uint256)",
+];
+
+const routerAbi = [
+ "function addLiquidityETH(address token,uint amountTokenDesired,uint amountTokenMin,uint amountETHMin,address to,uint deadline) payable returns (uint amountToken, uint amountETH, uint liquidity)",
+];
+
+const mintAbi = ["function mint(address to,uint256 amount)"];
+
+function defaultWethFloat() {
+ return parseFloat(process.env.CHAIN138_UNI_V2_DEFAULT_WETH || "0.25");
+}
+
+function stablePerWethUsd() {
+ return parseFloat(process.env.CHAIN138_UNI_V2_STABLE_PER_WETH || "2100");
+}
+
+async function ensureMinted(tokenAddr, signer, need) {
+ const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer);
+ const bal = await c.balanceOf(signer.address);
+ if (bal >= need) return;
+ const short = need - bal;
+ const m = await hre.ethers.getContractAt(mintAbi, tokenAddr, signer);
+ await (await m.mint(signer.address, short)).wait();
+}
+
+async function ensureBalance(tokenAddr, signer, need, meta) {
+ const c = await hre.ethers.getContractAt(erc20Abi, tokenAddr, signer);
+ const bal = await c.balanceOf(signer.address);
+ if (bal >= need) return;
+ try {
+ await ensureMinted(tokenAddr, signer, need);
+ } catch (e) {
+ throw new Error(
+ `${meta.symbol}: need ${hre.ethers.formatUnits(need - bal, meta.decimals)} more; mint failed: ${e.message}`
+ );
+ }
+}
+
+/** Ensure wallet has enough native ETH for addLiquidityETH `value` plus a gas buffer (this script does not wrap). */
+async function ensureNativeEth(signer, valueWei) {
+ const buffer = BigInt(process.env.CHAIN138_UNI_V2_ETH_GAS_BUFFER_WEI || "30000000000000000");
+ const need = valueWei + buffer;
+ const bal = await signer.provider.getBalance(signer.address);
+ if (bal < need) {
+ throw new Error(
+ `Native ETH: need at least ${hre.ethers.formatEther(need)} (${hre.ethers.formatEther(valueWei)} for pool + buffer); have ${hre.ethers.formatEther(bal)}`
+ );
+ }
+}
+
+function computeAmounts(meta) {
+ const envW = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_WETH`];
+ const envT = process.env[`CHAIN138_UNI_V2_SEED_${meta.key}_TOKEN`];
+
+ const ethWei =
+ envW !== undefined && envW !== ""
+ ? hre.ethers.parseEther(String(envW))
+ : hre.ethers.parseEther(String(defaultWethFloat()));
+
+ const wethEth = parseFloat(hre.ethers.formatEther(ethWei));
+
+ let tokenStr;
+ if (envT !== undefined && envT !== "") {
+ tokenStr = String(envT);
+ } else {
+ switch (meta.kind) {
+ case "stable6":
+ tokenStr = String(Math.max(1, Math.round(stablePerWethUsd() * wethEth)));
+ break;
+ case "link":
+ tokenStr = String(Math.max(1, Math.round(100 * wethEth)));
+ break;
+ case "weth10":
+ tokenStr = hre.ethers.formatEther(ethWei);
+ break;
+ case "xau6":
+ tokenStr = String(Math.max(1, Math.round(1 * Math.max(wethEth, 0.01))));
+ break;
+ default:
+ tokenStr = "1";
+ }
+ }
+
+ const tokenAmt = hre.ethers.parseUnits(tokenStr, meta.decimals);
+ return { ethWei, tokenAmt };
+}
+
+function minAmounts(amount, bps) {
+ return (amount * (10000n - bps)) / 10000n;
+}
+
+async function main() {
+ const signers = await hre.ethers.getSigners();
+ if (!signers?.length) {
+ throw new Error("chain138: no signer — set PRIVATE_KEY (32-byte hex) for network chain138 in env");
+ }
+ const deployer = signers[0];
+ const gasPrice = BigInt(process.env.CHAIN138_NATIVE_V2_GAS_PRICE || (await hre.ethers.provider.send("eth_gasPrice", [])));
+ const txOverrides = {
+ type: 0,
+ gasPrice,
+ gasLimit: BigInt(process.env.CHAIN138_NATIVE_V2_LIQUIDITY_GAS_LIMIT || "3000000"),
+ };
+
+ const deploymentPath = path.resolve(__dirname, "../../deployments/chain138/uniswap-v2-native.json");
+ let factory = (process.env.CHAIN138_UNISWAP_V2_EXISTING_FACTORY || "").trim();
+ let routerAddr = (process.env.CHAIN138_UNISWAP_V2_EXISTING_ROUTER || "").trim();
+ if (fs.existsSync(deploymentPath)) {
+ const j = JSON.parse(fs.readFileSync(deploymentPath, "utf8"));
+ if (!factory) factory = j.factory;
+ if (!routerAddr) routerAddr = j.router;
+ }
+ if (!factory) {
+ throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_FACTORY or add deployments/chain138/uniswap-v2-native.json");
+ }
+ if (!routerAddr) {
+ throw new Error("Set CHAIN138_UNISWAP_V2_EXISTING_ROUTER or add router to deployments/chain138/uniswap-v2-native.json");
+ }
+
+ const Factory = await hre.ethers.getContractFactory(
+ "contracts/vendor/uniswap-v2-core/UniswapV2Factory.sol:UniswapV2Factory"
+ );
+ const Pair = await hre.ethers.getContractFactory(
+ "contracts/vendor/uniswap-v2-core/UniswapV2Pair.sol:UniswapV2Pair"
+ );
+
+ const factoryC = Factory.attach(factory);
+ const wethAddr = (process.env.CHAIN138_NATIVE_WETH9 || process.env.CHAIN138_WETH9_ADDRESS || WETH9).trim();
+ const router = await hre.ethers.getContractAt(routerAbi, routerAddr, deployer);
+
+ console.log(
+ `[eth] add liquidity via router addLiquidityETH (native ETH \`value\`; no WETH9.deposit() in this script) router=${routerAddr}`
+ );
+ console.log(`[weth9] pair reserve token: ${wethAddr} (router wraps ETH inside addLiquidityETH)`);
+
+ const slipBps = BigInt(process.env.CHAIN138_UNI_V2_SLIPPAGE_BPS || "50");
+ const skipKeys = new Set(
+ (process.env.CHAIN138_UNI_V2_SKIP_KEYS || "")
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean)
+ );
+
+ /** @type {{ key: string, pair: string, status: string }[]} */
+ const report = [];
+
+ for (const meta of WETH_QUOTE_TARGETS) {
+ if (skipKeys.has(meta.key)) {
+ report.push({ key: meta.key, pair: "", status: "skip_env" });
+ console.log(`[skip] ${meta.symbol} (${meta.key}) via CHAIN138_UNI_V2_SKIP_KEYS`);
+ continue;
+ }
+ const tokenAddr = meta.address;
+ if (tokenAddr.toLowerCase() === wethAddr.toLowerCase()) continue;
+
+ const { ethWei, tokenAmt } = computeAmounts(meta);
+
+ let pairAddress = await factoryC.getPair(wethAddr, tokenAddr);
+ if (pairAddress === hre.ethers.ZeroAddress) {
+ const tx = await factoryC.createPair(wethAddr, tokenAddr, txOverrides);
+ await tx.wait();
+ pairAddress = await factoryC.getPair(wethAddr, tokenAddr);
+ }
+
+ const pair = Pair.connect(deployer).attach(pairAddress);
+ const [r0, r1] = await pair.getReserves();
+ if (r0 > 0n || r1 > 0n) {
+ report.push({ key: meta.key, pair: pairAddress, status: "skip_existing_liquidity" });
+ console.log(`[skip] ${meta.symbol} already funded ${pairAddress}`);
+ continue;
+ }
+
+ const tokenC = await hre.ethers.getContractAt(erc20Abi, tokenAddr, deployer);
+
+ await ensureNativeEth(deployer, ethWei);
+ await ensureBalance(tokenAddr, deployer, tokenAmt, meta);
+
+ const allowance = await tokenC.allowance(deployer.address, routerAddr);
+ if (allowance < tokenAmt) {
+ await (await tokenC.approve(routerAddr, hre.ethers.MaxUint256)).wait();
+ }
+
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
+ const tokenMin = minAmounts(tokenAmt, slipBps);
+ const ethMin = minAmounts(ethWei, slipBps);
+
+ console.log(`[eth] addLiquidityETH ${meta.symbol}: value=${hre.ethers.formatEther(ethWei)} ETH, token=${tokenAmt.toString()}`);
+ const tx = await router.addLiquidityETH(
+ tokenAddr,
+ tokenAmt,
+ tokenMin,
+ ethMin,
+ deployer.address,
+ deadline,
+ { ...txOverrides, value: ethWei }
+ );
+ await tx.wait();
+
+ report.push({ key: meta.key, pair: pairAddress, status: "seeded" });
+ console.log(`[ok] ${meta.symbol} ${pairAddress}`);
+ }
+
+ const out = {
+ chainId: 138,
+ deployer: deployer.address,
+ factory,
+ router: routerAddr,
+ weth9: wethAddr,
+ results: report,
+ };
+ const outPath = path.resolve(__dirname, "../../deployments/chain138/uni-v2-weth-quote-seed.json");
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
+ fs.writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
+ console.log(JSON.stringify(out, null, 2));
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/scripts/deployment/c138-cw-bridge-75-split.sh b/scripts/deployment/c138-cw-bridge-75-split.sh
new file mode 100755
index 0000000..c3236e3
--- /dev/null
+++ b/scripts/deployment/c138-cw-bridge-75-split.sh
@@ -0,0 +1,291 @@
+#!/usr/bin/env bash
+# Plan + verify Chain 138 → 10 networks: move 75% of each c* balance split evenly (7.5% per network).
+# Uses CWMultiTokenBridgeL1 on 138 (CW_L1_BRIDGE_CHAIN138) when routes are configured.
+#
+# Modes:
+# --plan-only Write JSON table + human summary (default)
+# --check-routes cast call supportedCanonicalToken + destinations per token×chain
+# --emit-cmds Print approve + lockAndSend cast lines (dry; review before running)
+# --help
+#
+# Env: PRIVATE_KEY, RPC_URL_138, CW_L1_BRIDGE_CHAIN138, smom-dbis-138/.env
+# Optional: RECIPIENT_ADDRESS (default: deployer), OUT_JSON (default: reports/status/c138-bridge-75-split-latest.json)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
+cd "$SMOM_ROOT"
+
+MODE="plan"
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --plan-only) MODE="plan" ;;
+ --check-routes) MODE="check" ;;
+ --emit-cmds) MODE="emit" ;;
+ --help|-h)
+ grep '^#' "$0" | head -20
+ exit 0
+ ;;
+ *) echo "Unknown: $1"; exit 1 ;;
+ esac
+ shift || true
+done
+
+if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
+ # shellcheck disable=SC1090
+ PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
+elif [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+
+RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
+OUT_JSON="${OUT_JSON:-$SMOM_ROOT/reports/status/c138-bridge-75-split-latest.json}"
+BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
+DEPLOYER=""
+if [[ -n "${PRIVATE_KEY:-}" ]]; then
+ DEPLOYER="$(cast wallet address "$PRIVATE_KEY" 2>/dev/null || true)"
+fi
+RECIPIENT="${RECIPIENT_ADDRESS:-$DEPLOYER}"
+export RPC OUT_JSON DEPLOYER RECIPIENT BRIDGE
+
+# CCIP chain selectors (Chainlink CCIP mainnet directory / repo BRIDGE_CONFIGURATION.md). Verify before prod.
+declare -A SELECTOR=(
+ [Mainnet]=5009297550715157269
+ [Optimism]=3734403246176062136
+ [Cronos]=1456215246176062136
+ [BSC]=11344663589394136015
+ [Gnosis]=465200170687744372
+ [Polygon]=4051577828743386545
+ [Base]=15971525489660198786
+ [Arbitrum]=4949039107694359620
+ [Celo]=1346049177634351622
+ [Avalanche]=6433500567565415381
+)
+
+# name:address (Compliant / canonical c* on 138)
+read -r -d '' TOKEN_ROWS << 'EOF' || true
+cUSDT:0x93E66202A11B1772E55407B32B44e5Cd8eda7f22
+cUSDC:0xf22258f57794CC8E06237084b353Ab30fFfa640b
+cUSDT_V2:0x8d342d321DdEe97D0c5011DAF8ca0B59DA617D29
+cUSDC_V2:0x1ac3F4942a71E86A9682D91837E1E71b7BACdF99
+cEURC:0x8085961F9cF02b4d800A3c6d386D31da4B34266a
+cEURT:0xdf4b71c61E5912712C1Bdd451416B9aC26949d72
+cGBPC:0x003960f16D9d34F2e98d62723B6721Fb92074aD2
+cGBPT:0x350f54e4D23795f86A9c03988c7135357CCaD97c
+cAUDC:0xD51482e567c03899eecE3CAe8a058161FD56069D
+cJPYC:0xEe269e1226a334182aace90056EE4ee5Cc8A6770
+cCHFC:0x873990849DDa5117d7C644f0aF24370797C03885
+cCADC:0x54dBd40cF05e15906A2C21f600937e96787f5679
+cAUDT:0xC034b8Ff3088f644D492E95619720ba8fB582933
+cJPYT:0x54fb3A6b16163D8cFa48EAff79205D1309B1a9A1
+cCHFT:0xd91f31725444dD1F53FA6dE236A5e90a8281d970
+cCADT:0xAb456be5Db1E1069F55F75E8c8fecAa6a71D1c8F
+cXAUC:0x290E52a8819A4fbD0714E517225429aA2B70EC6b
+cXAUT:0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E
+cAUSDT:0x5fdDF65733e3d590463F68f93Cf16E8c04081271
+EOF
+
+export TOKEN_ROWS
+mkdir -p "$(dirname "$OUT_JSON")"
+
+python3 << PY
+import json, subprocess, os, re, sys
+
+rpc = os.environ.get("RPC", "http://192.168.11.211:8545")
+deployer = os.environ.get("DEPLOYER", "")
+recipient = os.environ.get("RECIPIENT", deployer)
+bridge = os.environ.get("BRIDGE", "")
+
+selectors = {
+ "Mainnet": 5009297550715157269,
+ "Optimism": 3734403246176062136,
+ "Cronos": 1456215246176062136,
+ "BSC": 11344663589394136015,
+ "Gnosis": 465200170687744372,
+ "Polygon": 4051577828743386545,
+ "Base": 15971525489660198786,
+ "Arbitrum": 4949039107694359620,
+ "Celo": 1346049177634351622,
+ "Avalanche": 6433500567565415381,
+}
+n_chains = len(selectors)
+
+rows = []
+for line in os.environ.get("TOKEN_ROWS", "").strip().split("\n"):
+ if not line.strip() or line.startswith("#"):
+ continue
+ sym, addr = line.split(":", 1)
+ rows.append((sym.strip(), addr.strip()))
+
+def balance_of(addr):
+ if not deployer:
+ return None
+ r = subprocess.run(
+ ["cast", "call", addr, "balanceOf(address)(uint256)", deployer, "--rpc-url", rpc],
+ capture_output=True, text=True,
+ )
+ if r.returncode != 0:
+ return None
+ m = re.match(r"^\s*(\d+)", r.stdout.strip())
+ return int(m.group(1)) if m else None
+
+plan = {
+ "schema": "c138-bridge-75-split/v1",
+ "rpc_url": rpc,
+ "deployer": deployer,
+ "recipient": recipient,
+ "cw_l1_bridge": bridge,
+ "split": "75% of balance divided evenly across 10 networks (7.5% per chain, integer base units)",
+ "tokens": [],
+}
+
+for sym, addr in rows:
+ bal = balance_of(addr)
+ if bal is None:
+ plan["tokens"].append({"symbol": sym, "address": addr, "error": "balance_of_failed"})
+ continue
+ q75 = bal * 75 // 100
+ per = q75 // n_chains
+ rem = q75 % n_chains
+ entry = {
+ "symbol": sym,
+ "address": addr,
+ "balance_wei": str(bal),
+ "pct_75_wei": str(q75),
+ "per_chain_wei": str(per),
+ "remainder_wei": str(rem),
+ "chains": {},
+ }
+ for cname, sel in selectors.items():
+ entry["chains"][cname] = {"selector": str(sel), "amount_wei": str(per)}
+ plan["tokens"].append(entry)
+
+path = os.environ.get("OUT_JSON", "")
+with open(path, "w") as f:
+ json.dump(plan, f, indent=2)
+
+print(json.dumps({"written": path, "tokens": len(plan["tokens"])}))
+PY
+
+export RPC DEPLOYER RECIPIENT BRIDGE OUT_JSON
+export TOKEN_ROWS="$TOKEN_ROWS"
+
+python3 - <<'PY'
+import json, os
+with open(os.environ["OUT_JSON"]) as f:
+ p = json.load(f)
+print("\n=== c* 75% / 10 networks (per-chain amount, 6 dp human for fiat-style) ===\n")
+print(f"Deployer: {p.get('deployer','?')}\nRecipient: {p.get('recipient','?')}\nBridge: {p.get('cw_l1_bridge') or '(unset)'}\n")
+for t in p["tokens"]:
+ if "error" in t:
+ print(f"{t['symbol']}: {t['error']}")
+ continue
+ sym = t["symbol"]
+ per = int(t["per_chain_wei"])
+ if sym.startswith("cXAU"):
+ hu = per / 1e6
+ print(f"{sym:<10} per chain: {hu:,.6f} troy oz (wei={t['per_chain_wei']})")
+ else:
+ hu = per / 1e6
+ print(f"{sym:<10} per chain: {hu:,.6f} tokens (wei={t['per_chain_wei']})")
+print("\nJSON:", os.environ["OUT_JSON"])
+PY
+
+if [[ "$MODE" == "plan" ]]; then
+ exit 0
+fi
+
+[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138"; exit 1; }
+code=$(cast code "$BRIDGE" --rpc-url "$RPC" 2>/dev/null || echo "0x")
+[[ -n "$code" && "$code" != "0x" ]] || { echo "No contract at CW_L1_BRIDGE_CHAIN138=$BRIDGE"; exit 1; }
+
+if [[ "$MODE" == "check" ]]; then
+ echo ""
+ echo "=== Route checks: $BRIDGE ==="
+ while IFS= read -r line; do
+ [[ -z "$line" ]] && continue
+ sym="${line%%:*}"
+ addr="${line#*:}"
+ if sup_raw=$(cast call "$BRIDGE" "supportedCanonicalToken(address)(bool)" "$addr" --rpc-url "$RPC" 2>/dev/null); then
+ echo "$sym supported=$sup_raw"
+ else
+ echo "$sym supported=(query reverted — non-CW ABI or older build; rely on destinations below)"
+ fi
+ for net in Mainnet Optimism Cronos BSC Gnosis Polygon Base Arbitrum Celo Avalanche; do
+ sel="${SELECTOR[$net]}"
+ dest=$(cast call "$BRIDGE" "destinations(address,uint64)(address,bool)" "$addr" "$sel" --rpc-url "$RPC" 2>/dev/null || echo "ERR")
+ echo " $net ($sel): $dest"
+ done
+ done <<< "$TOKEN_ROWS"
+ exit 0
+fi
+
+if [[ "$MODE" == "emit" ]]; then
+ [[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required for --emit-cmds"; exit 1; }
+ [[ -n "$RECIPIENT" ]] || { echo "RECIPIENT_ADDRESS or deployer required"; exit 1; }
+ LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}"
+ [[ -n "$LINK_TOKEN" ]] || { echo "Set LINK_TOKEN or LINK_TOKEN_CHAIN138 for fee approval lines"; exit 1; }
+ export LINK_TOKEN
+ echo ""
+ echo "=== Review-only cast snippets (feeToken=LINK on this bridge: approve LINK, approve token, lockAndSend) ==="
+ OUT_CAST="${OUT_CAST:-$SMOM_ROOT/reports/status/c138-bridge-75-split-cast-commands.sh}"
+ export OUT_CAST
+ {
+ echo "#!/usr/bin/env bash"
+ echo "# Generated: c138-cw-bridge-75-split.sh --emit-cmds"
+ echo "# Review destinations + reserve verifier before running. Fund LINK for fees."
+ echo "set -euo pipefail"
+ # Quoted heredoc: do not let bash expand \$PRIVATE_KEY into the generated file.
+ python3 <<'PY'
+import json, os, subprocess
+rpc = os.environ["RPC"]
+bridge = os.environ["BRIDGE"]
+recipient = os.environ["RECIPIENT"]
+link = os.environ["LINK_TOKEN"]
+with open(os.environ["OUT_JSON"]) as f:
+ plan = json.load(f)
+selectors = {
+ "Mainnet": 5009297550715157269,
+ "Optimism": 3734403246176062136,
+ "Cronos": 1456215246176062136,
+ "BSC": 11344663589394136015,
+ "Gnosis": 465200170687744372,
+ "Polygon": 4051577828743386545,
+ "Base": 15971525489660198786,
+ "Arbitrum": 4949039107694359620,
+ "Celo": 1346049177634351622,
+ "Avalanche": 6433500567565415381,
+}
+for t in plan["tokens"]:
+ if "error" in t or int(t.get("per_chain_wei", 0)) == 0:
+ continue
+ sym, token, amt = t["symbol"], t["address"], t["per_chain_wei"]
+ for cname, sel in selectors.items():
+ chk = subprocess.run(
+ ["cast", "call", bridge, "destinations(address,uint64)(address,bool)", token, str(sel), "--rpc-url", rpc],
+ capture_output=True, text=True,
+ )
+ if chk.returncode != 0 or "true" not in chk.stdout:
+ continue
+ fee = subprocess.run(
+ ["cast", "call", bridge,
+ "calculateFee(address,uint64,address,uint256)(uint256)",
+ token, str(sel), recipient, amt,
+ "--rpc-url", rpc],
+ capture_output=True, text=True,
+ )
+ fq = fee.stdout.strip().split()[0] if fee.returncode == 0 else "0"
+ print("")
+ print(f"# {sym} -> {cname} selector={sel} amount={amt} fee_wei={fq}")
+ print(f"cast send {link} \"approve(address,uint256)\" {bridge} {fq} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 120000")
+ print(f"cast send {token} \"approve(address,uint256)\" {bridge} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 400000")
+ print(f"cast send {bridge} \"lockAndSend(address,uint64,address,uint256)\" {token} {sel} {recipient} {amt} --rpc-url {rpc} --private-key \"$PRIVATE_KEY\" --legacy --gas-limit 4000000")
+PY
+ } > "$OUT_CAST"
+ chmod +x "$OUT_CAST"
+ echo "Wrote enabled-routes-only commands: $OUT_CAST"
+ wc -l "$OUT_CAST"
+ exit 0
+fi
diff --git a/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh b/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh
new file mode 100755
index 0000000..360248e
--- /dev/null
+++ b/scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+# Register GRU v2 (CompliantFiatTokenV2) addresses on CWMultiTokenBridgeL1 and
+# configureDestination for the same 10 CCIP lanes as cUSDT/cUSDC (receivers from CW_BRIDGE_*).
+#
+# Note: The CWMultiTokenBridgeL1 deployed at CW_L1_BRIDGE_CHAIN138 on Chain 138 does **not**
+# include configureSupportedCanonicalToken in runtime bytecode (older build). Do not send that
+# call — it always reverts. Destinations + lockAndSend are the supported path.
+#
+# Usage:
+# bash scripts/deployment/cw-l1-bootstrap-gru-v2-ccip-routes.sh [--dry-run]
+#
+# Env: PROJECT_ROOT load via proxmox scripts/lib/load-project-env.sh or smom-dbis-138/.env
+# CW_L1_BRIDGE_CHAIN138, RPC_URL_138, PRIVATE_KEY (deployer = bridge admin)
+# CW_BRIDGE_MAINNET, CW_BRIDGE_OPTIMISM, CW_BRIDGE_CRONOS, CW_BRIDGE_BSC,
+# CW_BRIDGE_GNOSIS, CW_BRIDGE_POLYGON, CW_BRIDGE_BASE, CW_BRIDGE_ARBITRUM,
+# CW_BRIDGE_CELO, CW_BRIDGE_AVALANCHE
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
+cd "$SMOM_ROOT"
+
+DRY_RUN=0
+[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
+
+if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
+ # shellcheck disable=SC1090
+ PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
+elif [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+
+BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
+RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
+
+[[ -n "$BRIDGE" && "$BRIDGE" != "0x0000000000000000000000000000000000000000" ]] || {
+ echo "Set CW_L1_BRIDGE_CHAIN138" >&2
+ exit 1
+}
+[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required" >&2; exit 1; }
+
+# GRU v2 on Chain 138 (wallet-token-submissions.chain138.json)
+V2=(
+ "0x243e6581Dc8a98d98B92265858b322b193555C81"
+ "0x2bAFA83d8fF8BaE9505511998987D0659791605B"
+ "0x707508D223103f5D2d9EFBc656302c9d48878b29"
+ "0xee17c18E10E55ce23F7457D018aAa2Fb1E64B281"
+ "0xfb37aFd415B70C5cEDc9bA58a72D517207b769Bb"
+ "0x2c751bBE4f299b989b3A8c333E0A966cdcA6Fd98"
+ "0x60B7FB8e0DD0Be8595AD12Fe80AE832861Be747c"
+ "0xe799033c87fE0CE316DAECcefBE3134CC74b76a9"
+ "0xF0F0F81bE3D033D8586bAfd2293e37eE2f615647"
+ "0x89477E982847023aaB5C3492082cd1bB4b1b9Ef1"
+)
+
+# selector|receiver (must match c138-cw-bridge-75-split.sh / BRIDGE_CONFIGURATION)
+declare -a ROUTES=(
+ "5009297550715157269|${CW_BRIDGE_MAINNET:-}"
+ "3734403246176062136|${CW_BRIDGE_OPTIMISM:-}"
+ "1456215246176062136|${CW_BRIDGE_CRONOS:-}"
+ "11344663589394136015|${CW_BRIDGE_BSC:-}"
+ "465200170687744372|${CW_BRIDGE_GNOSIS:-}"
+ "4051577828743386545|${CW_BRIDGE_POLYGON:-}"
+ "15971525489660198786|${CW_BRIDGE_BASE:-}"
+ "4949039107694359620|${CW_BRIDGE_ARBITRUM:-}"
+ "1346049177634351622|${CW_BRIDGE_CELO:-}"
+ "6433500567565415381|${CW_BRIDGE_AVALANCHE:-}"
+)
+
+send() {
+ if [[ "$DRY_RUN" -eq 1 ]]; then
+ echo "[dry-run] $*"
+ return 0
+ fi
+ "$@"
+}
+
+echo "=== configureDestination (10 GRU v2 tokens x up to 10 chains) ==="
+for t in "${V2[@]}"; do
+ for row in "${ROUTES[@]}"; do
+ sel="${row%%|*}"
+ recv="${row#*|}"
+ if [[ -z "$recv" || "$recv" == "0x0000000000000000000000000000000000000000" ]]; then
+ echo "SKIP selector=$sel (receiver unset)"
+ continue
+ fi
+ echo "-> token=$t selector=$sel recv=$recv"
+ send cast send "$BRIDGE" "configureDestination(address,uint64,address,bool)" "$t" "$sel" "$recv" true \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 500000
+ done
+done
+
+echo ""
+echo "Done. Verify with: cast call $BRIDGE \"destinations(address,uint64)(address,bool)\" --rpc-url $RPC"
diff --git a/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh b/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh
new file mode 100755
index 0000000..57843b6
--- /dev/null
+++ b/scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh
@@ -0,0 +1,106 @@
+#!/usr/bin/env bash
+# After cw-l1-bootstrap-gru-v2-ccip-routes.sh: approve LINK + tokens, then lockAndSend
+# 90% / 10 per-chain amounts to RECIPIENT_ADDRESS (default Ulysse EVM).
+#
+# Usage:
+# RECIPIENT_ADDRESS=0x... bash scripts/deployment/cw-l1-bridge-gru-v2-90pct-lock-and-send.sh [--dry-run]
+#
+# Env: CW_L1_BRIDGE_CHAIN138, RPC_URL_138, PRIVATE_KEY
+# LINK: fee token on bridge (defaults to feeToken() on bridge)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
+cd "$SMOM_ROOT"
+
+DRY_RUN=0
+[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
+
+if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
+ PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
+elif [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+
+BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
+RPC="${RPC_URL_138:-http://192.168.11.211:8545}"
+RECIPIENT="${RECIPIENT_ADDRESS:-0xCbe669ECe9e3Da7dD1688207e545EDDA9a64A514}"
+
+[[ -n "$BRIDGE" ]] || { echo "Set CW_L1_BRIDGE_CHAIN138" >&2; exit 1; }
+[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required" >&2; exit 1; }
+
+LINK_TOKEN="${LINK_TOKEN_CHAIN138:-${LINK_TOKEN:-}}"
+if [[ -z "$LINK_TOKEN" ]]; then
+ LINK_TOKEN=$(cast call "$BRIDGE" "feeToken()(address)" --rpc-url "$RPC" 2>/dev/null | grep -oE '0x[a-fA-F0-9]{40}' | head -1)
+fi
+[[ -n "$LINK_TOKEN" && "$LINK_TOKEN" != "0x0000000000000000000000000000000000000000" ]] || {
+ echo "Could not resolve LINK fee token; set LINK_TOKEN_CHAIN138" >&2
+ exit 1
+}
+
+MAX_U256="115792089237316195423570985008687907853269984665640564039457584007913129639935"
+
+# token|per_chain_wei (90% / 10 floors from operator plan)
+declare -a PAIRS=(
+ "0x243e6581Dc8a98d98B92265858b322b193555C81|3822172740000"
+ "0x2bAFA83d8fF8BaE9505511998987D0659791605B|2778945930000"
+ "0x707508D223103f5D2d9EFBc656302c9d48878b29|4123219860000"
+ "0xee17c18E10E55ce23F7457D018aAa2Fb1E64B281|4744441350000"
+ "0xfb37aFd415B70C5cEDc9bA58a72D517207b769Bb|6522556410000"
+ "0x2c751bBE4f299b989b3A8c333E0A966cdcA6Fd98|3557466000000"
+ "0x60B7FB8e0DD0Be8595AD12Fe80AE832861Be747c|6810124410000"
+ "0xe799033c87fE0CE316DAECcefBE3134CC74b76a9|2471337720000"
+ "0xF0F0F81bE3D033D8586bAfd2293e37eE2f615647|2421904773"
+ "0x89477E982847023aaB5C3492082cd1bB4b1b9Ef1|2421904773"
+)
+
+# Same 10 selectors as bootstrap script (order must match)
+declare -a SELECTORS=(
+ 5009297550715157269
+ 3734403246176062136
+ 1456215246176062136
+ 11344663589394136015
+ 465200170687744372
+ 4051577828743386545
+ 15971525489660198786
+ 4949039107694359620
+ 1346049177634351622
+ 6433500567565415381
+)
+
+send() {
+ if [[ "$DRY_RUN" -eq 1 ]]; then
+ echo "[dry-run] $*"
+ return 0
+ fi
+ "$@"
+}
+
+echo "Bridge=$BRIDGE recipient=$RECIPIENT LINK=$LINK_TOKEN"
+echo "=== Approvals: LINK + each GRU v2 token to bridge (max) ==="
+send cast send "$LINK_TOKEN" "approve(address,uint256)" "$BRIDGE" "$MAX_U256" \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 120000
+
+for row in "${PAIRS[@]}"; do
+ token="${row%%|*}"
+ echo "approve token $token"
+ send cast send "$token" "approve(address,uint256)" "$BRIDGE" "$MAX_U256" \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 120000
+done
+
+echo ""
+echo "=== lockAndSend (10 tokens x 10 chains) ==="
+for row in "${PAIRS[@]}"; do
+ token="${row%%|*}"
+ amt="${row#*|}"
+ for sel in "${SELECTORS[@]}"; do
+ echo "lockAndSend token=$token selector=$sel amount=$amt"
+ send cast send "$BRIDGE" "lockAndSend(address,uint64,address,uint256)" \
+ "$token" "$sel" "$RECIPIENT" "$amt" \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit 5000000
+ done
+done
+
+echo "Done."
diff --git a/scripts/deployment/cw-l1-configure-destination-8-chains.sh b/scripts/deployment/cw-l1-configure-destination-8-chains.sh
new file mode 100755
index 0000000..7f6b9ca
--- /dev/null
+++ b/scripts/deployment/cw-l1-configure-destination-8-chains.sh
@@ -0,0 +1,118 @@
+#!/usr/bin/env bash
+# Admin template: CWMultiTokenBridgeL1.configureDestination for the eight chains
+# that are not Mainnet / Avalanche (those typically already wired for cUSDT/cUSDC).
+#
+# Uses receiver addresses from CW_BRIDGE_OPTIMISM, CW_BRIDGE_CRONOS, CW_BRIDGE_BSC,
+# CW_BRIDGE_GNOSIS, CW_BRIDGE_POLYGON, CW_BRIDGE_BASE, CW_BRIDGE_ARBITRUM, CW_BRIDGE_CELO
+# (see smom-dbis-138/.env). CCIP selectors match docs/deployment/BRIDGE_CONFIGURATION.md
+# and Chainlink CCIP directory — verify before mainnet use.
+#
+# Target contract: CW_L1_BRIDGE_CHAIN138 — onlyAdmin.
+#
+# Usage:
+# bash scripts/deployment/cw-l1-configure-destination-8-chains.sh [--print-only]
+# # default writes reports/status/cw-l1-configure-destination-8-chains.cast.sh
+#
+# Env:
+# CW_L1_BRIDGE_CHAIN138, RPC_URL_138 (or CHAIN138_RPC), PRIVATE_KEY (only if you execute the .sh)
+# CUSDT_ADDRESS_138, CUSDC_ADDRESS_138 (or override CANONICAL_TOKENS="0x...,0x...")
+#
+# Does not broadcast unless you run the generated file. Never commit private keys.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
+cd "$SMOM_ROOT"
+
+PRINT_ONLY=0
+[[ "${1:-}" == "--print-only" ]] && PRINT_ONLY=1
+
+if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
+ # shellcheck disable=SC1090
+ PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
+elif [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+
+BRIDGE="${CW_L1_BRIDGE_CHAIN138:-}"
+RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
+OUT_SH="${OUT_SH:-$SMOM_ROOT/reports/status/cw-l1-configure-destination-8-chains.cast.sh}"
+
+if [[ -z "$BRIDGE" || "$BRIDGE" == "0x0000000000000000000000000000000000000000" ]]; then
+ echo "Set CW_L1_BRIDGE_CHAIN138 in .env" >&2
+ exit 1
+fi
+
+# Remaining eight (exclude Mainnet + Avalanche)
+declare -a ROWS=(
+ "Optimism|3734403246176062136|${CW_BRIDGE_OPTIMISM:-}"
+ "Cronos|1456215246176062136|${CW_BRIDGE_CRONOS:-}"
+ "BSC|11344663589394136015|${CW_BRIDGE_BSC:-}"
+ "Gnosis|465200170687744372|${CW_BRIDGE_GNOSIS:-}"
+ "Polygon|4051577828743386545|${CW_BRIDGE_POLYGON:-}"
+ "Base|15971525489660198786|${CW_BRIDGE_BASE:-}"
+ "Arbitrum|4949039107694359620|${CW_BRIDGE_ARBITRUM:-}"
+ "Celo|1346049177634351622|${CW_BRIDGE_CELO:-}"
+)
+
+TOKENS_STR="${CANONICAL_TOKENS:-}"
+if [[ -z "$TOKENS_STR" ]]; then
+ T1="${CUSDT_ADDRESS_138:-${COMPLIANT_USDT_ADDRESS:-}}"
+ T2="${CUSDC_ADDRESS_138:-${COMPLIANT_USDC_ADDRESS:-}}"
+ TOKENS_STR="${T1},${T2}"
+fi
+
+IFS=',' read -r -a TOKEN_ADDRS <<< "$(echo "$TOKENS_STR" | tr -d '[:space:]')"
+[[ -n "${TOKEN_ADDRS[0]:-}" && "${TOKEN_ADDRS[0]}" == 0x* ]] || { echo "Set CUSDT_ADDRESS_138 or CANONICAL_TOKENS" >&2; exit 1; }
+
+gen() {
+ local sym="$1" token="$2"
+ echo ""
+ echo "# --- $sym $token ---"
+ for row in "${ROWS[@]}"; do
+ IFS='|' read -r name sel recv <<< "$row"
+ if [[ -z "$recv" || "$recv" == "0x0000000000000000000000000000000000000000" ]]; then
+ echo "# SKIP $name: CW_BRIDGE_* receiver unset"
+ continue
+ fi
+ echo "# $sym -> $name (selector $sel) receiver=$recv"
+ echo "cast send \"$BRIDGE\" \\"
+ echo " \"configureDestination(address,uint64,address,bool)\" \\"
+ echo " \"$token\" \\"
+ echo " \"$sel\" \\"
+ echo " \"$recv\" \\"
+ echo " true \\"
+ echo " --rpc-url \"$RPC\" \\"
+ echo " --private-key \"\$PRIVATE_KEY\" \\"
+ echo " --legacy \\"
+ echo " --gas-limit 500000"
+ echo ""
+ echo "# Verify: cast call \"$BRIDGE\" \"destinations(address,uint64)(address,bool)\" \"$token\" \"$sel\" --rpc-url \"$RPC\""
+ echo ""
+ done
+}
+
+{
+ echo "#!/usr/bin/env bash"
+ echo "# Generated: cw-l1-configure-destination-8-chains.sh"
+ echo "# Admin-only: configureDestination on CW L1 for eight chains (not Mainnet/Avalanche)."
+ echo "# Requires: admin key = deployer or bridge admin; source .env with PRIVATE_KEY."
+ echo "set -euo pipefail"
+ echo ""
+
+ if [[ -n "${TOKEN_ADDRS[1]:-}" && "${TOKEN_ADDRS[1]}" == 0x* ]]; then
+ gen "cUSDT" "${TOKEN_ADDRS[0]}"
+ gen "cUSDC" "${TOKEN_ADDRS[1]}"
+ else
+ gen "TOKEN" "${TOKEN_ADDRS[0]}"
+ fi
+} > "$OUT_SH"
+chmod +x "$OUT_SH"
+
+echo "Wrote: $OUT_SH"
+echo ""
+if [[ "$PRINT_ONLY" -eq 1 ]] || [[ -t 1 ]]; then
+ cat "$OUT_SH"
+fi
diff --git a/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh b/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh
new file mode 100755
index 0000000..fc1b4bd
--- /dev/null
+++ b/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# Deploy Tether-style ISO-4217 complements (cAUDT, cJPYT, cCHFT, cCADT) on Chain 138.
+#
+# Why not CREATE2 via CREATE2Factory (0x750E4a8adCe9f0e67A420aBE91342DC64Eb90825)?
+# On Core/Besu, factory.deploy(bytes,uint256) with CompliantFiatToken initcode consistently
+# OOGs (~2.6M–8M+ gas) even with high limits; eth_call traces succeed — use standard CREATE here.
+#
+# Uses scoped Forge (scripts/forge/scope.sh create tokens) so solc does not pull the full repo.
+# Compiles with --evm-version paris: Core 138 Besu may reject Cancun bytecode (invalid opcode 0x5f/PUSH0).
+#
+# Requires: PRIVATE_KEY; RPC via RPC_URL_138, CHAIN138_RPC, or RPC_URL (see below).
+# Idempotent: skips if contract already has code at the address from env (if set).
+#
+# Usage: from repo root:
+# bash smom-dbis-138/scripts/deployment/deploy-compliant-fiat-tether-iso-chain138.sh
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+PROXMOX_ROOT="$(cd "$SMOM_ROOT/.." && pwd)"
+cd "$SMOM_ROOT"
+
+if [[ -f "$PROXMOX_ROOT/scripts/lib/load-project-env.sh" ]]; then
+ # shellcheck disable=SC1090
+ PROJECT_ROOT="$PROXMOX_ROOT" source "$PROXMOX_ROOT/scripts/lib/load-project-env.sh"
+elif [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+
+[[ -n "${PRIVATE_KEY:-}" ]] || { echo "PRIVATE_KEY required"; exit 1; }
+command -v jq >/dev/null 2>&1 || { echo "jq required (for receipt status check)"; exit 1; }
+RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
+# Paris bytecode deploy ~2M gas; leave headroom for Besu.
+GAS_LIMIT_DEPLOY="${GAS_LIMIT_DEPLOY:-3500000}"
+EVM_VERSION_CHAIN138="${EVM_VERSION_CHAIN138:-paris}"
+OWNER="$(cast wallet address "$PRIVATE_KEY")"
+
+# Ensure scoped artifacts match chain EVM (Paris for Chain 138 Core).
+bash scripts/forge/scope.sh build tokens --evm-version "$EVM_VERSION_CHAIN138" --force
+
+declare -a SPECS=(
+ "cAUDT|Tether AUD (Compliant)|AUD|${CAUDT_ADDRESS_138:-}"
+ "cJPYT|Tether JPY (Compliant)|JPY|${CJPYT_ADDRESS_138:-}"
+ "cCHFT|Tether CHF (Compliant)|CHF|${CCHFT_ADDRESS_138:-}"
+ "cCADT|Tether CAD (Compliant)|CAD|${CCADT_ADDRESS_138:-}"
+)
+
+DEPLOYED_SNIPPET="# After deploy (paste into smom-dbis-138/.env next to other C*ADDRESS_138):"
+while IFS='|' read -r sym name cc existing; do
+ if [[ -n "$existing" && "$existing" != "0x0000000000000000000000000000000000000000" ]]; then
+ code=$(cast code "$existing" --rpc-url "$RPC" 2>/dev/null || echo "0x")
+ if [[ -n "$code" && "$code" != "0x" ]]; then
+ echo "Skip $sym (already deployed at $existing)"
+ continue
+ fi
+ fi
+ echo "Deploy $sym..."
+ out=$(bash scripts/forge/scope.sh create tokens contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy \
+ --evm-version "$EVM_VERSION_CHAIN138" \
+ --gas-limit "$GAS_LIMIT_DEPLOY" \
+ --broadcast \
+ --constructor-args "$name" "$sym" 6 "$cc" "$OWNER" "$OWNER" 1000000000000 2>&1) || {
+ echo "$out"
+ exit 1
+ }
+ echo "$out"
+ txh=$(printf '%s\n' "$out" | sed -n 's/.*Transaction hash: \(0x[0-9a-fA-F]\{64\}\).*/\1/p' | head -1)
+ if [[ -n "$txh" ]]; then
+ st=$(cast receipt "$txh" --json --rpc-url "$RPC" 2>/dev/null | jq -r '.status // "0x0"')
+ if [[ "$st" != "0x1" ]]; then
+ echo "error: tx $txh failed on-chain (status=$st). Raise GAS_LIMIT_DEPLOY or inspect: cast run $txh --rpc-url $RPC" >&2
+ exit 1
+ fi
+ fi
+ addr=$(printf '%s\n' "$out" | sed -n 's/.*Deployed to: \(0x[0-9a-fA-F]\{40\}\).*/\1/p' | head -1)
+ if [[ -z "$addr" ]]; then
+ addr=$(printf '%s\n' "$out" | sed -n 's/.*"contract":"\(0x[0-9a-fA-F]\{40\}\)".*/\1/p' | head -1)
+ fi
+ if [[ -n "$addr" ]]; then
+ ccode=$(cast code "$addr" --rpc-url "$RPC" 2>/dev/null || echo "0x")
+ if [[ -z "$ccode" || "$ccode" == "0x" ]]; then
+ echo "error: no code at $addr after successful receipt — aborting" >&2
+ exit 1
+ fi
+ fi
+ if [[ -n "$addr" ]]; then
+ var=""
+ case "$sym" in
+ cAUDT) var="CAUDT_ADDRESS_138" ;;
+ cJPYT) var="CJPYT_ADDRESS_138" ;;
+ cCHFT) var="CCHFT_ADDRESS_138" ;;
+ cCADT) var="CCADT_ADDRESS_138" ;;
+ esac
+ DEPLOYED_SNIPPET+=$'\n'"${var}=${addr}"
+ fi
+done < <(printf '%s\n' "${SPECS[@]}")
+
+echo ""
+echo "$DEPLOYED_SNIPPET"
+echo ""
+echo "Verify: cast code --rpc-url \"\$RPC_URL_138\""
+echo "Optional mint (random 26M–85M per token, deployer must hold minter role):"
+echo " bash smom-dbis-138/scripts/mint-compliant-fiat-tether-iso-138.sh"
diff --git a/scripts/deployment/deploy-pmm-all-l2s.sh b/scripts/deployment/deploy-pmm-all-l2s.sh
index 230181e..828a961 100755
--- a/scripts/deployment/deploy-pmm-all-l2s.sh
+++ b/scripts/deployment/deploy-pmm-all-l2s.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# Deploy DODOPMMIntegration on each L2 (BSC, Polygon, Base, Optimism, Arbitrum, Avalanche, Cronos, Gnosis).
+# Deploy DODOPMMIntegration on each L2/public chain (BSC, Polygon, Base, Optimism, Arbitrum, Avalanche, Cronos, Gnosis, Celo).
# Uses .env for RPC and token addresses. Chains via tag: --chain bsc polygon ... (or DEPLOY_PMM_L2S_FILTER in .env).
# Usage: ./scripts/deployment/deploy-pmm-all-l2s.sh [--chain bsc polygon base]
set -euo pipefail
@@ -37,6 +37,7 @@ CHAINS=(
"AVALANCHE:43114:AVALANCHE_RPC_URL"
"CRONOS:25:CRONOS_RPC_URL"
"GNOSIS:100:GNOSIS_MAINNET_RPC"
+ "CELO:42220:CELO_MAINNET_RPC"
)
for entry in "${CHAINS[@]}"; do
@@ -59,14 +60,16 @@ for entry in "${CHAINS[@]}"; do
cusdc_var="${name}_COMPLIANT_USDC_ADDRESS"
cusdt_var_alt="COMPLIANT_USDT_${name}"
cusdc_var_alt="COMPLIANT_USDC_${name}"
+ cwusdt_var="CWUSDT_${name}"
+ cwusdc_var="CWUSDC_${name}"
# Per-chain cUSDT/cUSDC (optional): CUSDT_ADDRESS_ / CUSDC_ADDRESS_ or POLYGON_COMPLIANT_USDT_ADDRESS etc.
cusdt_chain="CUSDT_ADDRESS_${chain_id}"
cusdc_chain="CUSDC_ADDRESS_${chain_id}"
dvm="$(first_set_env "$dvm_var" "DODO_VENDING_MACHINE_ADDRESS" || true)"
usdt="$(first_set_env "$usdt_var" "$usdt_var_alt" "OFFICIAL_USDT_ADDRESS" || true)"
usdc="$(first_set_env "$usdc_var" "$usdc_var_alt" "OFFICIAL_USDC_ADDRESS" || true)"
- compliant_usdt="$(first_set_env "$cusdt_var" "$cusdt_var_alt" "$cusdt_chain" || true)"
- compliant_usdc="$(first_set_env "$cusdc_var" "$cusdc_var_alt" "$cusdc_chain" || true)"
+ compliant_usdt="$(first_set_env "$cusdt_var" "$cusdt_var_alt" "$cusdt_chain" "$cwusdt_var" || true)"
+ compliant_usdc="$(first_set_env "$cusdc_var" "$cusdc_var_alt" "$cusdc_chain" "$cwusdc_var" || true)"
compliant_usdt="${compliant_usdt:-$usdt}"
compliant_usdc="${compliant_usdc:-$usdc}"
diff --git a/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh b/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh
new file mode 100755
index 0000000..7b7cb28
--- /dev/null
+++ b/scripts/deployment/seed-chain138-uni-v2-weth-quote-pairs.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# Seed Uniswap V2 pools on Chain 138: each canonical asset vs WETH (see script header).
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+cd "$ROOT"
+if [[ -f scripts/load-env.sh ]]; then
+ # shellcheck source=/dev/null
+ source scripts/load-env.sh
+fi
+export CHAIN138_RPC_URL="${CHAIN138_RPC_URL:-${RPC_URL_138:-http://192.168.11.211:8545}}"
+exec npx hardhat run scripts/chain138/seed-uni-v2-weth-quote-pairs.js --network chain138
diff --git a/scripts/deployment/verify-wemix-bridges.sh b/scripts/deployment/verify-wemix-bridges.sh
new file mode 100755
index 0000000..ef8e8b1
--- /dev/null
+++ b/scripts/deployment/verify-wemix-bridges.sh
@@ -0,0 +1,119 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+cd "$PROJECT_ROOT"
+
+if [[ -f "$SCRIPT_DIR/../lib/deployment/dotenv.sh" ]]; then
+ # shellcheck disable=SC1090
+ source "$SCRIPT_DIR/../lib/deployment/dotenv.sh"
+ load_deployment_env --repo-root "${PROJECT_ROOT}"
+elif [[ -f "$PROJECT_ROOT/.env" ]]; then
+ set -a
+ # shellcheck disable=SC1090
+ source "$PROJECT_ROOT/.env"
+ set +a
+fi
+
+require_env() {
+ local name="$1"
+ if [[ -z "${!name:-}" ]]; then
+ echo "ERROR: $name is required" >&2
+ exit 1
+ fi
+}
+
+require_env WEMIX_RPC
+require_env CCIPWETH9_BRIDGE_WEMIX
+require_env CCIPWETH10_BRIDGE_WEMIX
+require_env CCIP_ROUTER_WEMIX
+require_env WETH9_WEMIX
+require_env WETH10_WEMIX
+require_env LINK_TOKEN_WEMIX
+require_env WEMIXSCAN_API_KEY
+
+flatten_b64() {
+ local source_file="$1"
+ forge flatten "$source_file" | python3 -c 'import sys,base64; print(base64.b64encode(sys.stdin.buffer.read()).decode())'
+}
+
+constructor_args() {
+ local weth="$1"
+ cast abi-encode 'constructor(address,address,address)' \
+ "$CCIP_ROUTER_WEMIX" \
+ "$weth" \
+ "$LINK_TOKEN_WEMIX" | sed 's/^0x//'
+}
+
+submit_verify() {
+ local address="$1"
+ local contract_name="$2"
+ local source_file="$3"
+ local weth="$4"
+
+ local payload
+ payload="$(mktemp)"
+
+ python3 - "$payload" "$contract_name" "$(flatten_b64 "$source_file")" "$(constructor_args "$weth")" <<'PY'
+import json
+import sys
+from pathlib import Path
+
+path, contract_name, contract_source, constructor_arguments = sys.argv[1:]
+body = {
+ "contract_name": contract_name,
+ "compiler": "v0.8.20+commit.a1b79de6",
+ "runs_optimizer": 200,
+ "optimization_enabled": 1,
+ "contract_source": contract_source,
+ "constructor_arguments": constructor_arguments,
+ "libraries": [],
+}
+Path(path).write_text(json.dumps(body))
+PY
+
+ echo "Submitting verification for $contract_name at $address..."
+ local response_file
+ response_file="$(mktemp)"
+ local http_code
+ http_code="$(
+ curl --max-time 90 \
+ -sS \
+ -o "$response_file" \
+ -w '%{http_code}' \
+ -H "api-key: ${WEMIXSCAN_API_KEY}" \
+ -H 'Content-Type: application/json' \
+ -X POST \
+ --data @"$payload" \
+ "https://explorerapi.wemix.com/v1/contracts/${address}/verify"
+ )"
+
+ echo "HTTP ${http_code}"
+ cat "$response_file"
+ echo
+
+ if [[ "$http_code" == "200" ]]; then
+ echo "Verification request accepted for $contract_name"
+ elif [[ "$http_code" == "504" ]]; then
+ echo "WARNING: Wemix explorer timed out while compiling $contract_name. Check the explorer code tab manually." >&2
+ else
+ echo "WARNING: Verification not accepted for $contract_name" >&2
+ fi
+}
+
+check_status() {
+ local address="$1"
+ echo "Checking explorer code endpoint for $address..."
+ curl --max-time 30 \
+ -sS \
+ -H "api-key: ${WEMIXSCAN_API_KEY}" \
+ "https://explorerapi.wemix.com/v1/contracts/${address}/code" || true
+ echo
+}
+
+submit_verify "$CCIPWETH9_BRIDGE_WEMIX" "CCIPWETH9Bridge" "contracts/ccip/CCIPWETH9Bridge.sol" "$WETH9_WEMIX"
+submit_verify "$CCIPWETH10_BRIDGE_WEMIX" "CCIPWETH10Bridge" "contracts/ccip/CCIPWETH10Bridge.sol" "$WETH10_WEMIX"
+
+check_status "$CCIPWETH9_BRIDGE_WEMIX"
+check_status "$CCIPWETH10_BRIDGE_WEMIX"
diff --git a/scripts/forge/scope.sh b/scripts/forge/scope.sh
index b6d964d..5bf5126 100755
--- a/scripts/forge/scope.sh
+++ b/scripts/forge/scope.sh
@@ -44,12 +44,14 @@ Usage:
bash scripts/forge/scope.sh build [scope] [forge build args...]
bash scripts/forge/scope.sh test [scope] [forge test args...]
bash scripts/forge/scope.sh script [scope] [forge script args...]
+ bash scripts/forge/scope.sh create [scope] [forge create args...]
bash scripts/forge/scope.sh orphans [--json]
Examples:
bash scripts/forge/scope.sh build treasury
bash scripts/forge/scope.sh test flash --match-path 'test/flash/*.t.sol'
bash scripts/forge/scope.sh script bridge/trustless script/bridge/trustless/DeployTrustlessBridge.s.sol:DeployTrustlessBridge --rpc-url "$RPC_URL_138"
+ bash scripts/forge/scope.sh create tokens contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken --rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY" --legacy
FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/*.t.sol'
Notes:
@@ -266,7 +268,7 @@ main() {
orphans)
exec python3 scripts/forge/report-contract-reachability.py "$@"
;;
- build|test|script)
+ build|test|script|create)
local scope=""
if [[ $# -gt 0 && "$1" != --* ]]; then
local maybe_scope
@@ -313,6 +315,10 @@ main() {
[[ $# -gt 0 ]] || die "script command requires a script target, e.g. script/treasury/DeployTreasuryExecutor138.s.sol:DeployTreasuryExecutor138"
exec forge script "$@"
;;
+ create)
+ [[ $# -gt 0 ]] || die "create command requires a contract target, e.g. contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken"
+ exec forge create "$@"
+ ;;
esac
;;
*)
diff --git a/scripts/mint-compliant-fiat-tether-iso-138.sh b/scripts/mint-compliant-fiat-tether-iso-138.sh
new file mode 100755
index 0000000..cb9a910
--- /dev/null
+++ b/scripts/mint-compliant-fiat-tether-iso-138.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# Mint CompliantFiatToken Tether-style ISO complements on Chain 138 (cAUDT, cJPYT, cCHFT, cCADT).
+# Uses addresses from smom-dbis-138/.env: CAUDT_ADDRESS_138, CJPYT_ADDRESS_138, CCHFT_ADDRESS_138, CCADT_ADDRESS_138.
+#
+# Default: random whole-number amount per token between 26_000_000 and 85_000_000 (6 decimals).
+# Override: ./scripts/mint-compliant-fiat-tether-iso-138.sh (same amount for all four)
+#
+# Requires: PRIVATE_KEY (minter/deployer), RPC. Skips any address unset or zero.
+# Requires: cast (foundry).
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+SMOM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+PROXMOX_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+cd "$SMOM_ROOT"
+
+if [[ -f .env ]]; then
+ set -a && source .env && set +a
+fi
+if [[ -z "${PRIVATE_KEY:-}" && -f "${PROXMOX_ROOT}/scripts/lib/load-project-env.sh" ]]; then
+ # shellcheck disable=SC1090
+ PROJECT_ROOT="$PROXMOX_ROOT" source "${PROXMOX_ROOT}/scripts/lib/load-project-env.sh"
+ cd "$SMOM_ROOT"
+fi
+
+RPC="${RPC_URL_138:-${CHAIN138_RPC:-${RPC_URL:-http://192.168.11.211:8545}}}"
+GAS_LIMIT_MINT="${GAS_LIMIT_MINT:-400000}"
+
+[ -n "${PRIVATE_KEY:-}" ] || { echo "PRIVATE_KEY not set"; exit 1; }
+DEPLOYER=$(cast wallet address "$PRIVATE_KEY" 2>/dev/null) || exit 1
+
+if [[ -n "${1:-}" ]]; then
+ AMOUNT_HUMAN="$1"
+elif command -v shuf >/dev/null 2>&1; then
+ AMOUNT_HUMAN=$(shuf -i 26000000-85000000 -n 1)
+elif command -v python3 >/dev/null 2>&1; then
+ AMOUNT_HUMAN=$(python3 -c 'import random; print(random.randint(26_000_000, 85_000_000))')
+else
+ echo "Install coreutils (shuf) or python3 for random mint amounts, or pass amount_human as \$1" >&2
+ exit 1
+fi
+BASE_UNITS=$((AMOUNT_HUMAN * 1000000))
+
+echo "=== Mint Tether ISO complements (Chain 138) ==="
+echo " Deployer: $DEPLOYER"
+echo " Amount: $AMOUNT_HUMAN tokens each ($BASE_UNITS base units)"
+echo " RPC: $RPC"
+echo ""
+
+for pair in \
+ "cAUDT:${CAUDT_ADDRESS_138:-}" \
+ "cJPYT:${CJPYT_ADDRESS_138:-}" \
+ "cCHFT:${CCHFT_ADDRESS_138:-}" \
+ "cCADT:${CCADT_ADDRESS_138:-}"; do
+ sym="${pair%%:*}"
+ addr="${pair#*:}"
+ if [[ -z "$addr" || "$addr" == "0x0000000000000000000000000000000000000000" ]]; then
+ echo "Skip $sym (address not set in .env)"
+ continue
+ fi
+ echo -n "Mint $sym ($addr)... "
+ if cast send "$addr" "mint(address,uint256)" "$DEPLOYER" "$BASE_UNITS" \
+ --rpc-url "$RPC" --private-key "$PRIVATE_KEY" --legacy --gas-limit "$GAS_LIMIT_MINT" 2>/dev/null; then
+ echo OK
+ else
+ echo FAIL
+ fi
+done
+echo "Done."
diff --git a/scripts/verify-tezos-etherlink-support.js b/scripts/verify-tezos-etherlink-support.js
index 13cd805..e519921 100644
--- a/scripts/verify-tezos-etherlink-support.js
+++ b/scripts/verify-tezos-etherlink-support.js
@@ -4,27 +4,59 @@
const LIFI_URL = 'https://li.quest/v1/chains';
const CCIP_URL = 'https://docs.chain.link/ccip/supported-networks';
+async function fetchText(url) {
+ const res = await fetch(url, {
+ headers: {
+ 'user-agent': 'proxmox-operator-support-check/1.0',
+ accept: 'text/html,application/json;q=0.9,*/*;q=0.8',
+ },
+ });
+ if (!res.ok) throw new Error(`${url} returned ${res.status}`);
+ return res.text();
+}
+
+async function fetchJson(url) {
+ const res = await fetch(url, {
+ headers: {
+ 'user-agent': 'proxmox-operator-support-check/1.0',
+ accept: 'application/json',
+ },
+ });
+ if (!res.ok) throw new Error(`${url} returned ${res.status}`);
+ return res.json();
+}
+
async function main() {
console.log('Tezos/Etherlink support check\n');
try {
- const lifiRes = await fetch(LIFI_URL);
- const lifi = await lifiRes.json();
- const ids = (lifi.chains || []).map((c) => c.id || c.chainId).filter(Boolean);
+ const lifi = await fetchJson(LIFI_URL);
+ const chains = lifi.chains || [];
+ const ids = chains.map((c) => c.id || c.chainId).filter(Boolean);
+ const etherlink = chains.find((c) => (c.id || c.chainId) === 42793);
console.log('LiFi chains include 138:', ids.includes(138));
console.log('LiFi chains include 42793:', ids.includes(42793));
console.log('LiFi chains include 651940:', ids.includes(651940));
+ if (etherlink) {
+ console.log('LiFi Etherlink entry:', JSON.stringify({
+ id: etherlink.id || etherlink.chainId,
+ key: etherlink.key,
+ name: etherlink.name,
+ coin: etherlink.coin,
+ mainnet: etherlink.mainnet,
+ }));
+ }
} catch (e) {
console.log('LiFi error:', e.message);
}
try {
- const ccipRes = await fetch(CCIP_URL);
- const html = await ccipRes.text();
- console.log('\nCCIP page mentions 42793:', html.includes('42793') || html.toLowerCase().includes('etherlink'));
- console.log('CCIP page mentions 138:', html.includes('138'));
+ const html = await fetchText(CCIP_URL);
+ const hasEtherlinkMention = html.includes('42793') || html.toLowerCase().includes('etherlink');
+ console.log('\nCCIP page mentions 42793/Etherlink:', hasEtherlinkMention);
+ console.log('CCIP page mention is not treated as support without selector/router confirmation.');
} catch (e) {
console.log('CCIP error:', e.message);
}
- console.log('\nJumper: verify manually; see docs/07-ccip/TEZOS_JUMPER_SUPPORT_MATRIX.md');
+ console.log('\nJumper: still verify manually; see docs/07-ccip/TEZOS_JUMPER_SUPPORT_MATRIX.md');
}
main().catch((e) => { console.error(e); process.exit(1); });
diff --git a/services/etherlink-relay/src/config.js b/services/etherlink-relay/src/config.js
index 8b92e20..f094158 100644
--- a/services/etherlink-relay/src/config.js
+++ b/services/etherlink-relay/src/config.js
@@ -14,8 +14,22 @@ export const config = {
sourceChain: { rpcUrl: process.env.RPC_URL_138 || process.env.RPC_URL || "http://127.0.0.1:8545" },
etherlinkRpcUrl: process.env.ETHERLINK_RPC_URL || "https://node.mainnet.etherlink.com",
etherlinkRelayBridge: process.env.ETHERLINK_RELAY_BRIDGE || "",
- relayPrivateKey: process.env.ETHERLINK_RELAY_PRIVATE_KEY || process.env.PRIVATE_KEY,
+ relayPrivateKey: resolvePrivateKey(
+ process.env.ETHERLINK_RELAY_PRIVATE_KEY,
+ process.env.PRIVATE_KEY,
+ process.env.DEPLOYER_PRIVATE_KEY,
+ ),
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || "5000", 10),
maxConcurrent: parseInt(process.env.ETHERLINK_RELAY_MAX_CONCURRENT || "5", 10),
queueDepthLimit: parseInt(process.env.ETHERLINK_RELAY_QUEUE_DEPTH || "100", 10),
};
+
+function resolvePrivateKey(...candidates) {
+ for (const candidate of candidates) {
+ if (!candidate) continue;
+ const value = candidate.trim();
+ if (!value || value.includes('${')) continue;
+ if (/^0x[0-9a-fA-F]{64}$/.test(value)) return value;
+ }
+ return "";
+}
diff --git a/services/non-evm-relay/lifecycle.ts b/services/non-evm-relay/lifecycle.ts
new file mode 100644
index 0000000..16dcce9
--- /dev/null
+++ b/services/non-evm-relay/lifecycle.ts
@@ -0,0 +1,53 @@
+export const NON_EVM_RELAY_LIFECYCLE = [
+ 'initiate',
+ 'observe_destination',
+ 'confirm_finalize',
+ 'reject_replay',
+ 'refund_recover'
+] as const;
+
+export type NonEvmRelayLifecycleStep = (typeof NON_EVM_RELAY_LIFECYCLE)[number];
+
+export type NonEvmExposureStatus =
+ | 'planned'
+ | 'operator_ready'
+ | 'public_ready'
+ | 'live'
+ | 'gated_by_chain138_prerequisites';
+
+export interface NonEvmNetworkPolicy {
+ identifier: string;
+ relayMode: 'custom_relay' | 'custom_relay_scaffold' | 'parallel_program';
+ destinationProgramModel: string;
+ signerFundingPolicy: string;
+ finalityPolicy: string;
+ publicExposureStatus: Exclude;
+}
+
+export interface NonEvmRelayObservation {
+ requestId: string;
+ lifecycleStep: NonEvmRelayLifecycleStep;
+ destinationReference?: string;
+ fulfillmentId?: string;
+ finalityValue?: number;
+ replayRejected?: boolean;
+}
+
+export function deriveExposureStatus(
+ policy: NonEvmNetworkPolicy,
+ checks: {
+ adapterPresent: boolean;
+ relaySurfacePresent: boolean;
+ chain138PrerequisitesReady: boolean;
+ }
+): NonEvmExposureStatus {
+ if (!checks.adapterPresent || !checks.relaySurfacePresent) {
+ return 'planned';
+ }
+
+ if (!checks.chain138PrerequisitesReady) {
+ return 'gated_by_chain138_prerequisites';
+ }
+
+ return policy.publicExposureStatus;
+}
diff --git a/services/solana-relay/.env.example b/services/solana-relay/.env.example
new file mode 100644
index 0000000..0f278c0
--- /dev/null
+++ b/services/solana-relay/.env.example
@@ -0,0 +1,11 @@
+SOLANA_RELAY_RUNTIME_CONFIG=../../config/solana-relay-runtime.json
+SOLANA_OPERATOR_HOST=192.168.11.11
+SOLANA_CLUSTER=mainnet-beta
+SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
+SOLANA_KEYPAIR_PATH=/root/.config/solana/id.json
+SOLANA_OPERATOR_WALLET=26HRdYbob5LmVM638jfpDd6ELyHz6TqP25YHVd9dGT4A
+SOLANA_DESTINATION_WALLET=26HRdYbob5LmVM638jfpDd6ELyHz6TqP25YHVd9dGT4A
+SOLANA_ADAPTER_ADDRESS=
+SOLANA_MIN_FINALITY_SLOTS=32
+CHAIN138_RPC_URL=http://192.168.11.211:8545
+CHAIN_REGISTRY_ADDRESS=0x6949137625CA923A4e9C80D5bc7DF673f9bbb84F
diff --git a/services/solana-relay/package.json b/services/solana-relay/package.json
new file mode 100644
index 0000000..8c315e6
--- /dev/null
+++ b/services/solana-relay/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "solana-relay",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "relay": "node src/relay-worker.mjs",
+ "relay:once": "node src/relay-worker.mjs --once"
+ },
+ "dependencies": {
+ "dotenv": "^16.6.1",
+ "ethers": "^6.15.0"
+ }
+}
diff --git a/services/solana-relay/src/SolanaRelayService.ts b/services/solana-relay/src/SolanaRelayService.ts
new file mode 100644
index 0000000..591df3d
--- /dev/null
+++ b/services/solana-relay/src/SolanaRelayService.ts
@@ -0,0 +1,116 @@
+import {
+ NON_EVM_RELAY_LIFECYCLE,
+ type NonEvmNetworkPolicy,
+ type NonEvmRelayObservation
+} from '../../non-evm-relay/lifecycle';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export const SOLANA_RELAY_POLICY: NonEvmNetworkPolicy = {
+ identifier: 'Solana',
+ relayMode: 'custom_relay',
+ destinationProgramModel: 'spl_or_bridge_wrapped_cw',
+ signerFundingPolicy: 'sol_operator_signer',
+ finalityPolicy: 'slot_finality>=32',
+ publicExposureStatus: 'live'
+};
+
+export interface SolanaRelayAssetRuntime {
+ chain138Symbol: string;
+ sourceToken: string;
+ solanaSymbol: string;
+ solanaMint: string;
+ decimals: number;
+ smokeTestAmountRaw: string;
+}
+
+export interface SolanaRelayRuntimeConfig {
+ sourceChain: {
+ chainId: number;
+ chainRegistryAddress: string;
+ registryIdentifier: string;
+ adapterAddress: string | null;
+ };
+ solana: {
+ cluster: string;
+ rpcUrl: string;
+ operatorWallet: string;
+ confirmationFinality: number;
+ recipientEncoding: string;
+ };
+ relay: {
+ mode: string;
+ runtimeConfigEnv: string;
+ syntheticConfirmationPrefix: string;
+ publicExposureStatus?: string;
+ workerEntryPoint?: string;
+ };
+ smokeTests: {
+ defaultAssets: string[];
+ wethRawAmount: string;
+ gruRawAmount: string;
+ requireAdapterRegistration: boolean;
+ };
+ assets: SolanaRelayAssetRuntime[];
+}
+
+export function resolveSolanaRelayRuntimeConfigPath(): string {
+ const explicit = process.env.SOLANA_RELAY_RUNTIME_CONFIG;
+ if (explicit && explicit.trim() !== '') {
+ return path.resolve(process.cwd(), explicit);
+ }
+
+ return path.resolve(__dirname, '../../../../config/solana-relay-runtime.json');
+}
+
+export function loadSolanaRelayRuntimeConfig(): SolanaRelayRuntimeConfig {
+ const runtimePath = resolveSolanaRelayRuntimeConfigPath();
+ const raw = fs.readFileSync(runtimePath, 'utf8');
+ return JSON.parse(raw) as SolanaRelayRuntimeConfig;
+}
+
+/**
+ * Shared lifecycle surface for the Solana relay worker.
+ * The actual host-side worker entry point lives in relay-worker.mjs and performs
+ * real Solana settlement plus Chain 138 finalization.
+ */
+export class SolanaRelayService {
+ readonly lifecycle = NON_EVM_RELAY_LIFECYCLE;
+ readonly runtime: SolanaRelayRuntimeConfig;
+
+ constructor(runtime = loadSolanaRelayRuntimeConfig()) {
+ this.runtime = runtime;
+ }
+
+ recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation {
+ return {
+ ...observation,
+ lifecycleStep: observation.lifecycleStep
+ };
+ }
+
+ getAsset(symbol: string): SolanaRelayAssetRuntime | undefined {
+ return this.runtime.assets.find((asset) => asset.solanaSymbol === symbol || asset.chain138Symbol === symbol);
+ }
+
+ buildSmokeObservation(
+ requestId: string,
+ symbol: string,
+ destinationReference?: string
+ ): NonEvmRelayObservation {
+ const asset = this.getAsset(symbol);
+ const destination = destinationReference || asset?.solanaMint || this.runtime.solana.operatorWallet;
+
+ return {
+ requestId,
+ lifecycleStep: 'confirm_finalize',
+ destinationReference: destination,
+ fulfillmentId: `${this.runtime.relay.syntheticConfirmationPrefix}:${symbol}:${requestId}`,
+ finalityValue: this.runtime.solana.confirmationFinality,
+ replayRejected: false
+ };
+ }
+}
diff --git a/services/solana-relay/src/relay-worker.mjs b/services/solana-relay/src/relay-worker.mjs
new file mode 100644
index 0000000..76aed57
--- /dev/null
+++ b/services/solana-relay/src/relay-worker.mjs
@@ -0,0 +1,239 @@
+import fs from 'fs';
+import path from 'path';
+import process from 'process';
+import { execFileSync } from 'child_process';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import { ethers } from 'ethers';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const serviceRoot = path.resolve(__dirname, '..');
+const repoRoot = path.resolve(serviceRoot, '..', '..', '..');
+const stateDir = path.join(serviceRoot, 'state');
+const defaultStatePath = path.join(stateDir, 'relay-state.json');
+const defaultHealthPath = path.join(repoRoot, 'reports', 'status', 'solana-relay-worker-health.json');
+
+dotenv.config({ path: path.join(repoRoot, '.env') });
+dotenv.config({ path: path.join(repoRoot, 'smom-dbis-138/.env') });
+dotenv.config({ path: path.join(process.env.HOME || '', '.secure-secrets', 'private-keys.env'), override: true });
+
+function readJson(filePath, fallback) {
+ try {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
+ } catch {
+ return fallback;
+ }
+}
+
+function writeJson(filePath, value) {
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
+}
+
+function loadRuntime() {
+ const runtimePath = process.env.SOLANA_RELAY_RUNTIME_CONFIG
+ ? path.resolve(process.cwd(), process.env.SOLANA_RELAY_RUNTIME_CONFIG)
+ : path.join(repoRoot, 'config', 'solana-relay-runtime.json');
+ return {
+ path: runtimePath,
+ config: JSON.parse(fs.readFileSync(runtimePath, 'utf8'))
+ };
+}
+
+function normalizeAddress(value) {
+ return ethers.getAddress(String(value).toLowerCase());
+}
+
+function rawToUiAmount(rawValue, decimals) {
+ const raw = BigInt(rawValue);
+ const base = 10n ** BigInt(decimals);
+ const whole = raw / base;
+ const fraction = raw % base;
+ if (fraction === 0n) return whole.toString();
+ const fractionString = fraction.toString().padStart(decimals, '0').replace(/0+$/, '');
+ return `${whole.toString()}.${fractionString}`;
+}
+
+function decodeRecipient(destination, recipientBytes) {
+ if (destination && destination.trim() !== '') return destination.trim();
+ if (!recipientBytes || recipientBytes === '0x') return null;
+ return ethers.toUtf8String(recipientBytes).trim();
+}
+
+function runLocalCommand(command, args) {
+ return execFileSync(command, args, {
+ cwd: repoRoot,
+ encoding: 'utf8',
+ env: process.env
+ }).trim();
+}
+
+function ensureAta(mint, wallet, keypairPath) {
+ try {
+ runLocalCommand('spl-token', ['create-account', mint, '--owner', wallet, '--fee-payer', keypairPath]);
+ } catch {
+ // No-op when the associated token account already exists.
+ }
+}
+
+function mintToDestination(mint, uiAmount, wallet, keypairPath) {
+ ensureAta(mint, wallet, keypairPath);
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ try {
+ const output = runLocalCommand('spl-token', [
+ 'mint',
+ mint,
+ uiAmount,
+ '--mint-authority',
+ keypairPath,
+ '--recipient-owner',
+ wallet
+ ]);
+ const match = output.match(/Signature:\s+(\S+)/);
+ if (match) return match[1];
+ } catch {
+ // Retry with a fresh blockhash on transient Solana RPC errors.
+ }
+ }
+ throw new Error(`Unable to parse Solana mint signature for ${mint}`);
+}
+
+function confirmSolanaSignature(signature) {
+ try {
+ return runLocalCommand('solana', ['confirm', signature, '--output', 'json']);
+ } catch (error) {
+ throw new Error(`Solana confirmation failed for ${signature}: ${error.message}`);
+ }
+}
+
+function buildFulfillmentId(requestId, solanaSymbol, solanaMint) {
+ return ethers.keccak256(
+ ethers.solidityPacked(['bytes32', 'string', 'string'], [requestId, solanaSymbol, solanaMint])
+ );
+}
+
+async function main() {
+ const once = process.argv.includes('--once');
+ const runtimeBundle = loadRuntime();
+ const runtime = runtimeBundle.config;
+ const rpcUrl = process.env.RPC_URL_138 || process.env.CHAIN138_RPC_URL || runtime.sourceChain.rpcEnv;
+ const privateKey = process.env.PRIVATE_KEY;
+ if (!privateKey || privateKey.includes('${')) {
+ throw new Error('PRIVATE_KEY is required');
+ }
+
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
+ const signer = new ethers.Wallet(privateKey, provider);
+ const adapterAddress = normalizeAddress(runtime.sourceChain.adapterAddress);
+ const adapterAbi = [
+ 'function confirmTransaction(bytes32 requestId,string txSignature,uint256 finalizedSlot,bytes32 fulfillmentId)',
+ 'function getBridgeStatus(bytes32 requestId) view returns (tuple(address sender,address token,uint256 amount,bytes destinationData,bytes32 requestId,uint8 status,uint256 createdAt,uint256 completedAt))',
+ 'event SolanaBridgeInitiated(bytes32 indexed requestId,address indexed sender,address indexed token,uint256 amount,string destination,bytes recipient)'
+ ];
+ const adapter = new ethers.Contract(adapterAddress, adapterAbi, signer);
+ const interfaceDecoder = adapter.interface;
+ const assetBySourceToken = new Map(
+ (runtime.assets || []).map((asset) => [normalizeAddress(asset.sourceToken), asset])
+ );
+ const state = readJson(defaultStatePath, { lastBlock: 0, processed: {} });
+ const latestBlock = await provider.getBlockNumber();
+ const fromBlock = Math.max(0, Number(state.lastBlock || 0) + 1);
+ const health = {
+ generatedAt: new Date().toISOString(),
+ runtimeConfig: runtimeBundle.path,
+ adapterAddress,
+ signer: signer.address,
+ fromBlock,
+ latestBlock,
+ processedThisRun: [],
+ mode: once ? 'once' : 'poll'
+ };
+
+ if (fromBlock > latestBlock) {
+ state.lastBlock = latestBlock;
+ writeJson(defaultStatePath, state);
+ writeJson(defaultHealthPath, health);
+ return;
+ }
+
+ const logs = await provider.getLogs({
+ address: adapterAddress,
+ fromBlock,
+ toBlock: latestBlock,
+ topics: [ethers.id('SolanaBridgeInitiated(bytes32,address,address,uint256,string,bytes)')]
+ });
+
+ for (const log of logs) {
+ const parsed = interfaceDecoder.parseLog(log);
+ const requestId = parsed.args.requestId;
+ if (state.processed[requestId]) continue;
+
+ const request = await adapter.getBridgeStatus(requestId);
+ if (Number(request.status) !== 1) continue;
+
+ const sourceToken = normalizeAddress(parsed.args.token);
+ const asset = assetBySourceToken.get(sourceToken);
+ if (!asset) {
+ throw new Error(`No Solana runtime asset for source token ${sourceToken}`);
+ }
+
+ const recipientWallet = decodeRecipient(parsed.args.destination, parsed.args.recipient);
+ if (!recipientWallet) {
+ throw new Error(`Missing Solana recipient for request ${requestId}`);
+ }
+
+ const uiAmount = rawToUiAmount(request.amount.toString(), asset.decimals);
+ const solanaSignature = mintToDestination(
+ asset.solanaMint,
+ uiAmount,
+ recipientWallet,
+ runtime.solana.keypairPath
+ );
+ confirmSolanaSignature(solanaSignature);
+
+ const fulfillmentId = buildFulfillmentId(requestId, asset.solanaSymbol, asset.solanaMint);
+ const confirmTx = await adapter.confirmTransaction(
+ requestId,
+ solanaSignature,
+ BigInt(runtime.solana.confirmationFinality),
+ fulfillmentId,
+ {
+ type: 0,
+ gasPrice: BigInt(process.env.CHAIN138_TX_GAS_PRICE || '200000')
+ }
+ );
+ const confirmReceipt = await confirmTx.wait();
+
+ state.processed[requestId] = {
+ blockNumber: log.blockNumber,
+ sourceToken,
+ solanaSymbol: asset.solanaSymbol,
+ solanaMint: asset.solanaMint,
+ recipientWallet,
+ requestAmountRaw: request.amount.toString(),
+ solanaSignature,
+ confirmTxHash: confirmReceipt.hash,
+ processedAt: new Date().toISOString()
+ };
+ health.processedThisRun.push({
+ requestId,
+ solanaSymbol: asset.solanaSymbol,
+ solanaSignature,
+ confirmTxHash: confirmReceipt.hash
+ });
+ }
+
+ state.lastBlock = latestBlock;
+ writeJson(defaultStatePath, state);
+ writeJson(defaultHealthPath, health);
+}
+
+main().catch((error) => {
+ writeJson(defaultHealthPath, {
+ generatedAt: new Date().toISOString(),
+ status: 'error',
+ error: error.message
+ });
+ console.error(error);
+ process.exit(1);
+});
diff --git a/services/stellar-relay/src/StellarRelayService.ts b/services/stellar-relay/src/StellarRelayService.ts
new file mode 100644
index 0000000..79954b4
--- /dev/null
+++ b/services/stellar-relay/src/StellarRelayService.ts
@@ -0,0 +1,22 @@
+import {
+ NON_EVM_RELAY_LIFECYCLE,
+ type NonEvmNetworkPolicy,
+ type NonEvmRelayObservation
+} from '../../non-evm-relay/lifecycle';
+
+export const STELLAR_RELAY_POLICY: NonEvmNetworkPolicy = {
+ identifier: 'Stellar',
+ relayMode: 'custom_relay_scaffold',
+ destinationProgramModel: 'stellar_issued_or_wrapped_cw',
+ signerFundingPolicy: 'xlm_operator_signer',
+ finalityPolicy: 'ledger_finality>=1',
+ publicExposureStatus: 'operator_ready'
+};
+
+export class StellarRelayService {
+ readonly lifecycle = NON_EVM_RELAY_LIFECYCLE;
+
+ recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation {
+ return observation;
+ }
+}
diff --git a/services/tezos-relay/src/config.js b/services/tezos-relay/src/config.js
index 4906e45..b540438 100644
--- a/services/tezos-relay/src/config.js
+++ b/services/tezos-relay/src/config.js
@@ -15,8 +15,22 @@ export const config = {
tezosAdapterAddress: process.env.TEZOS_ADAPTER_ADDRESS || '',
tezosRpcUrl: process.env.TEZOS_RPC_URL || 'https://mainnet.smartpy.io',
tezosMinterAddress: process.env.TEZOS_MINTER_ADDRESS || '',
- oraclePrivateKey: process.env.TEZOS_RELAY_ORACLE_KEY || process.env.PRIVATE_KEY,
+ oraclePrivateKey: resolvePrivateKey(
+ process.env.TEZOS_RELAY_ORACLE_KEY,
+ process.env.PRIVATE_KEY,
+ process.env.DEPLOYER_PRIVATE_KEY,
+ ),
pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || '5000', 10),
maxConcurrent: parseInt(process.env.TEZOS_RELAY_MAX_CONCURRENT || '5', 10),
mockTezosRelay: process.env.MOCK_TEZOS_RELAY === 'true',
};
+
+function resolvePrivateKey(...candidates) {
+ for (const candidate of candidates) {
+ if (!candidate) continue;
+ const value = candidate.trim();
+ if (!value || value.includes('${')) continue;
+ if (/^0x[0-9a-fA-F]{64}$/.test(value)) return value;
+ }
+ return '';
+}
diff --git a/services/token-aggregation/README.md b/services/token-aggregation/README.md
index 8c253e9..33ea7ee 100644
--- a/services/token-aggregation/README.md
+++ b/services/token-aggregation/README.md
@@ -230,14 +230,29 @@ Configure DEX factory addresses in `src/config/dex-factories.ts` or via environm
```bash
# ChainID 138
CHAIN_138_DODO_POOL_MANAGER=0x...
-CHAIN_138_UNISWAP_V2_FACTORY=0x...
-CHAIN_138_UNISWAP_V3_FACTORY=0x...
+CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279
+CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638
+CHAIN_138_UNISWAP_V2_START_BLOCK=4041370
+CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE
+CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34
+CHAIN_138_SUSHISWAP_START_BLOCK=4041495
+CHAIN_138_UNISWAP_V3_FACTORY=0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C
+CHAIN_138_UNISWAP_V3_ROUTER=0xde9cD8ee2811E6E64a41D5F68Be315d33995975E
# ChainID 651940
CHAIN_651940_UNISWAP_V2_FACTORY=0x...
+CHAIN_651940_UNISWAP_V2_ROUTER=0x...
+CHAIN_651940_UNISWAP_V2_START_BLOCK=0
CHAIN_651940_UNISWAP_V3_FACTORY=0x...
+CHAIN_651940_UNISWAP_V3_ROUTER=0x...
+CHAIN_651940_UNISWAP_V3_START_BLOCK=0
+CHAIN_651940_HYDX_FACTORY=0x...
+CHAIN_651940_HYDX_ROUTER=0x...
+CHAIN_651940_HYDX_START_BLOCK=0
```
+For ALL Mainnet non-DODO discovery, the repo now treats `HYDX` as the canonical custom venue surface when factory/router details are known. The broader `651940` non-DODO inventory is tracked in `config/allmainnet-non-dodo-protocol-surface.json`.
+
## Monitoring
The service includes:
diff --git a/services/token-aggregation/docs/DEPLOYMENT.md b/services/token-aggregation/docs/DEPLOYMENT.md
index 5709423..3116f25 100644
--- a/services/token-aggregation/docs/DEPLOYMENT.md
+++ b/services/token-aggregation/docs/DEPLOYMENT.md
@@ -175,15 +175,30 @@ For ChainID 138, configure DODO PoolManager address:
```bash
CHAIN_138_DODO_POOL_MANAGER=0x...
+CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279
+CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638
+CHAIN_138_UNISWAP_V2_START_BLOCK=4041370
+CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE
+CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34
+CHAIN_138_SUSHISWAP_START_BLOCK=4041495
```
For ChainID 651940, configure DEX factories as they are discovered:
```bash
CHAIN_651940_UNISWAP_V2_FACTORY=0x...
+CHAIN_651940_UNISWAP_V2_ROUTER=0x...
+CHAIN_651940_UNISWAP_V2_START_BLOCK=0
CHAIN_651940_UNISWAP_V3_FACTORY=0x...
+CHAIN_651940_UNISWAP_V3_ROUTER=0x...
+CHAIN_651940_UNISWAP_V3_START_BLOCK=0
+CHAIN_651940_HYDX_FACTORY=0x...
+CHAIN_651940_HYDX_ROUTER=0x...
+CHAIN_651940_HYDX_START_BLOCK=0
```
+The canonical ALL Mainnet non-DODO inventory is also tracked in the parent repo at `config/allmainnet-non-dodo-protocol-surface.json`.
+
## Monitoring
### Health Checks
diff --git a/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts b/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts
new file mode 100644
index 0000000..70e8259
--- /dev/null
+++ b/services/token-aggregation/src/adapters/dexscreener-adapter.test.ts
@@ -0,0 +1,99 @@
+import {
+ aggregateDexScreenerPairsToMarketData,
+ normalizeDexScreenerTokenPairsPayload,
+ type DexScreenerPair,
+} from './dexscreener-adapter';
+
+describe('normalizeDexScreenerTokenPairsPayload', () => {
+ it('accepts a raw JSON array (current API)', () => {
+ const pairs: DexScreenerPair[] = [
+ {
+ chainId: 'ethereum',
+ dexId: 'x',
+ url: '',
+ pairAddress: '0x',
+ baseToken: { address: '0xa', name: '', symbol: 'A' },
+ quoteToken: { address: '0xb', name: '', symbol: 'B' },
+ priceUsd: '1',
+ },
+ ];
+ expect(normalizeDexScreenerTokenPairsPayload(pairs)).toEqual(pairs);
+ });
+
+ it('accepts legacy { pairs: [...] } shape', () => {
+ const inner: DexScreenerPair[] = [
+ {
+ chainId: 'ethereum',
+ dexId: 'x',
+ url: '',
+ pairAddress: '0x',
+ baseToken: { address: '0xa', name: '', symbol: 'A' },
+ quoteToken: { address: '0xb', name: '', symbol: 'B' },
+ priceUsd: '2',
+ },
+ ];
+ expect(normalizeDexScreenerTokenPairsPayload({ pairs: inner })).toEqual(inner);
+ });
+});
+
+describe('aggregateDexScreenerPairsToMarketData', () => {
+ it('returns null for empty input', () => {
+ expect(aggregateDexScreenerPairsToMarketData(null)).toBeNull();
+ expect(aggregateDexScreenerPairsToMarketData([])).toBeNull();
+ });
+
+ it('picks price from the pair with highest USD liquidity', () => {
+ const pairs: DexScreenerPair[] = [
+ {
+ chainId: 'ethereum',
+ dexId: 'uniswap',
+ url: '',
+ pairAddress: '0x1',
+ baseToken: { address: '0xt', name: '', symbol: 'T' },
+ quoteToken: { address: '0xq', name: '', symbol: 'Q' },
+ priceUsd: '100',
+ liquidity: { usd: 1000 },
+ },
+ {
+ chainId: 'ethereum',
+ dexId: 'sushi',
+ url: '',
+ pairAddress: '0x2',
+ baseToken: { address: '0xt', name: '', symbol: 'T' },
+ quoteToken: { address: '0xq', name: '', symbol: 'Q' },
+ priceUsd: '1',
+ liquidity: { usd: 1_000_000 },
+ },
+ ];
+ const m = aggregateDexScreenerPairsToMarketData(pairs);
+ expect(m?.priceUsd).toBe(1);
+ expect(m?.liquidityUsd).toBe(1_001_000);
+ });
+
+ it('when liquidity is missing, prefers higher 24h volume', () => {
+ const pairs: DexScreenerPair[] = [
+ {
+ chainId: 'ethereum',
+ dexId: 'a',
+ url: '',
+ pairAddress: '0x1',
+ baseToken: { address: '0xt', name: '', symbol: 'T' },
+ quoteToken: { address: '0xq', name: '', symbol: 'Q' },
+ priceUsd: '50',
+ volume: { h24: 100 },
+ },
+ {
+ chainId: 'ethereum',
+ dexId: 'b',
+ url: '',
+ pairAddress: '0x2',
+ baseToken: { address: '0xt', name: '', symbol: 'T' },
+ quoteToken: { address: '0xq', name: '', symbol: 'Q' },
+ priceUsd: '51',
+ volume: { h24: 1_000_000 },
+ },
+ ];
+ const m = aggregateDexScreenerPairsToMarketData(pairs);
+ expect(m?.priceUsd).toBe(51);
+ });
+});
diff --git a/services/token-aggregation/src/adapters/dexscreener-adapter.ts b/services/token-aggregation/src/adapters/dexscreener-adapter.ts
index bfaaf12..86d4fa4 100644
--- a/services/token-aggregation/src/adapters/dexscreener-adapter.ts
+++ b/services/token-aggregation/src/adapters/dexscreener-adapter.ts
@@ -1,8 +1,9 @@
import axios, { AxiosInstance } from 'axios';
import { ExternalApiAdapter, TokenMetadata, MarketData } from './base-adapter';
+import { preferGruV2OfficialDexPairs } from '../config/gru-v2-deployment-pools';
import { logger } from '../utils/logger';
-interface DexScreenerPair {
+export interface DexScreenerPair {
chainId: string;
dexId: string;
url: string;
@@ -59,11 +60,23 @@ interface DexScreenerPair {
}
interface DexScreenerResponse {
- schemaVersion: string;
- pairs: DexScreenerPair[] | null;
+ schemaVersion?: string;
+ pairs?: DexScreenerPair[] | null;
pair?: DexScreenerPair;
}
+/** Current API returns a JSON array of pairs; older clients used `{ pairs: [...] }`. */
+export function normalizeDexScreenerTokenPairsPayload(data: unknown): DexScreenerPair[] {
+ if (Array.isArray(data)) {
+ return data as DexScreenerPair[];
+ }
+ if (data && typeof data === 'object' && data !== null && 'pairs' in data) {
+ const p = (data as DexScreenerResponse).pairs;
+ return Array.isArray(p) ? p : [];
+ }
+ return [];
+}
+
// Chain ID to DexScreener chain identifier mapping
// DexScreener uses chain identifiers like 'ethereum', 'bsc', etc.
const CHAIN_TO_DEXSCREENER_ID: Record = {
@@ -74,7 +87,11 @@ const CHAIN_TO_DEXSCREENER_ID: Record = {
42161: 'arbitrum',
10: 'optimism',
8453: 'base',
- // Note: 138 and 651940 are likely not supported
+ 100: 'gnosis',
+ 42220: 'celo',
+ 25: 'cronos',
+ 1111: 'wemix',
+ // Chain 138 / ALL Mainnet: not listed on public DexScreener API
};
// Reverse mapping for lookup
@@ -83,6 +100,55 @@ Object.entries(CHAIN_TO_DEXSCREENER_ID).forEach(([chainId, dexId]) => {
DEXSCREENER_ID_TO_CHAIN[dexId] = parseInt(chainId, 10);
});
+/** Prefer the pair with the most USD liquidity, then highest 24h volume (avoids averaging thin pools). */
+function pairLiquidityScore(pair: DexScreenerPair): number {
+ const liq = pair.liquidity?.usd ?? 0;
+ if (liq > 0) return liq;
+ return pair.volume?.h24 ?? 0;
+}
+
+/**
+ * Aggregates DexScreener token-pairs response into a single {@link MarketData} snapshot.
+ * Exported for unit tests.
+ */
+export function aggregateDexScreenerPairsToMarketData(pairs: DexScreenerPair[] | null | undefined): MarketData | null {
+ if (!pairs || pairs.length === 0) {
+ return null;
+ }
+
+ let totalVolume24h = 0;
+ let totalLiquidity = 0;
+ for (const pair of pairs) {
+ if (pair.volume?.h24) totalVolume24h += pair.volume.h24;
+ if (pair.liquidity?.usd) totalLiquidity += pair.liquidity.usd;
+ }
+
+ const priced = pairs.filter((p) => p.priceUsd && !Number.isNaN(parseFloat(p.priceUsd)));
+ if (priced.length === 0) {
+ return null;
+ }
+
+ let best = priced[0]!;
+ let bestScore = pairLiquidityScore(best);
+ for (let i = 1; i < priced.length; i++) {
+ const p = priced[i]!;
+ const s = pairLiquidityScore(p);
+ if (s > bestScore) {
+ best = p;
+ bestScore = s;
+ }
+ }
+
+ const priceUsd = parseFloat(best.priceUsd!);
+
+ return {
+ priceUsd: Number.isFinite(priceUsd) ? priceUsd : undefined,
+ volume24h: totalVolume24h > 0 ? totalVolume24h : undefined,
+ liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined,
+ lastUpdated: new Date(),
+ };
+}
+
export class DexScreenerAdapter implements ExternalApiAdapter {
private api: AxiosInstance;
private apiKey?: string;
@@ -131,11 +197,10 @@ export class DexScreenerAdapter implements ExternalApiAdapter {
return false;
}
- const response = await this.api.get(
- `/token-pairs/v1/${dexId}/${testAddress}`
- );
+ const response = await this.api.get(`/token-pairs/v1/${dexId}/${testAddress}`);
- const supported = response.status === 200 && (response.data.pairs?.length ?? 0) > 0;
+ const pairs = normalizeDexScreenerTokenPairsPayload(response.data);
+ const supported = response.status === 200 && pairs.length > 0;
this.cache.set(cacheKey, {
data: supported,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hour cache
@@ -181,39 +246,17 @@ export class DexScreenerAdapter implements ExternalApiAdapter {
}
try {
- const response = await this.api.get(
- `/token-pairs/v1/${dexId}/${address.toLowerCase()}`
- );
+ const response = await this.api.get(`/token-pairs/v1/${dexId}/${address.toLowerCase()}`);
- if (!response.data.pairs || response.data.pairs.length === 0) {
+ const pairsRaw = normalizeDexScreenerTokenPairsPayload(response.data);
+ if (pairsRaw.length === 0) {
+ return null;
+ }
+ const pairs = preferGruV2OfficialDexPairs(chainId, address.toLowerCase(), pairsRaw);
+ const marketData = aggregateDexScreenerPairsToMarketData(pairs);
+ if (!marketData) {
return null;
}
-
- // Aggregate data from all pairs
- let totalVolume24h = 0;
- let totalLiquidity = 0;
- let avgPrice = 0;
- let priceCount = 0;
- response.data.pairs.forEach((pair) => {
- if (pair.priceUsd) {
- avgPrice += parseFloat(pair.priceUsd);
- priceCount++;
- }
- if (pair.volume?.h24) {
- totalVolume24h += pair.volume.h24;
- }
- if (pair.liquidity?.usd) {
- totalLiquidity += pair.liquidity.usd;
- }
- // txns h24 available on pair.txns?.h24 for future use
- });
-
- const marketData: MarketData = {
- priceUsd: priceCount > 0 ? avgPrice / priceCount : undefined,
- volume24h: totalVolume24h > 0 ? totalVolume24h : undefined,
- liquidityUsd: totalLiquidity > 0 ? totalLiquidity : undefined,
- lastUpdated: new Date(),
- };
// Cache for 2 minutes (DexScreener updates frequently)
this.cache.set(cacheKey, {
@@ -242,11 +285,10 @@ export class DexScreenerAdapter implements ExternalApiAdapter {
}
try {
- const response = await this.api.get(
- `/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}`
- );
+ const response = await this.api.get(`/token-pairs/v1/${dexId}/${tokenAddress.toLowerCase()}`);
- return response.data.pairs || [];
+ const raw = normalizeDexScreenerTokenPairsPayload(response.data);
+ return preferGruV2OfficialDexPairs(chainId, tokenAddress.toLowerCase(), raw);
} catch (error) {
logger.error(`Error fetching DexScreener pairs for ${tokenAddress} on chain ${chainId}:`, error);
return [];
@@ -263,11 +305,11 @@ export class DexScreenerAdapter implements ExternalApiAdapter {
}
try {
- const response = await this.api.get(
+ const response = await this.api.get<{ pair?: DexScreenerPair }>(
`/latest/dex/pairs/${dexId}/${pairAddress.toLowerCase()}`
);
- return response.data.pair || null;
+ return response.data.pair ?? null;
} catch (error: unknown) {
const err = error as { response?: { status?: number } };
if (err.response?.status === 404) {
@@ -300,13 +342,13 @@ export class DexScreenerAdapter implements ExternalApiAdapter {
for (const chunk of chunks) {
try {
- const response = await this.api.get(
+ const response = await this.api.get(
`/tokens/v1/${dexId}/${chunk.map((addr) => addr.toLowerCase()).join(',')}`
);
- if (response.data.pairs) {
- // Group pairs by token address
- response.data.pairs.forEach((pair) => {
+ const batchPairs = normalizeDexScreenerTokenPairsPayload(response.data);
+ if (batchPairs.length > 0) {
+ batchPairs.forEach((pair) => {
const baseAddr = pair.baseToken.address.toLowerCase();
const quoteAddr = pair.quoteToken.address.toLowerCase();
diff --git a/services/token-aggregation/src/api/routes/report.test.ts b/services/token-aggregation/src/api/routes/report.test.ts
index f519577..ef70e3c 100644
--- a/services/token-aggregation/src/api/routes/report.test.ts
+++ b/services/token-aggregation/src/api/routes/report.test.ts
@@ -6,6 +6,7 @@
import { createServer } from 'http';
import express from 'express';
import reportRoutes from './report';
+import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
jest.mock('../../database/repositories/token-repo', () => ({
TokenRepository: jest.fn().mockImplementation(() => ({
@@ -124,6 +125,30 @@ describe('Report API', () => {
])
);
});
+
+ it('fills canonical fallback usd pricing when market data is absent', async () => {
+ const weth = getCanonicalTokenBySymbol(138, 'WETH');
+ expect(weth?.addresses[138]).toBeTruthy();
+ const wethAddress = String(weth?.addresses[138]).toLowerCase();
+
+ const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`);
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ const tokens138 = body.tokens?.['138'];
+ expect(Array.isArray(tokens138)).toBe(true);
+
+ const wethEntry = tokens138.find((token: Record) => token.address === wethAddress);
+ expect(wethEntry).toMatchObject({
+ symbol: 'WETH',
+ decimals: 18,
+ market: expect.objectContaining({
+ priceUsd: 2490,
+ volume24h: 0,
+ liquidityUsd: 0,
+ lastUpdated: '2026-04-15T00:00:00.000Z',
+ }),
+ });
+ });
});
describe('GET /api/v1/report/gas-registry', () => {
@@ -413,6 +438,68 @@ describe('Report API', () => {
});
});
+ describe('GET /api/v1/report/gru-v2-pmm-pools', () => {
+ it('returns resolved PMM pools from deployment-status when file is set', async () => {
+ const previousPath = process.env.DEPLOYMENT_STATUS_JSON_PATH;
+ const tempPath = `/tmp/token-aggregation-gru-v2-pmm-${Date.now()}.json`;
+
+ process.env.DEPLOYMENT_STATUS_JSON_PATH = tempPath;
+ await import('fs/promises').then((fs) =>
+ fs.writeFile(
+ tempPath,
+ JSON.stringify(
+ {
+ version: 'test-gru-pools',
+ updated: '2026-04-18',
+ homeChainId: 138,
+ chains: {
+ '1': {
+ name: 'Ethereum Mainnet',
+ cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' },
+ anchorAddresses: { USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' },
+ pmmPools: [
+ {
+ base: 'cWUSDT',
+ quote: 'USDC',
+ poolAddress: '0x1111111111111111111111111111111111111111',
+ feeBps: 3,
+ role: 'public_routing',
+ publicRoutingEnabled: true,
+ },
+ ],
+ },
+ },
+ },
+ null,
+ 2
+ )
+ )
+ );
+
+ try {
+ const res = await fetch(`${baseUrl}/api/v1/report/gru-v2-pmm-pools?chainId=1`);
+ expect(res.status).toBe(200);
+ const body = (await res.json()) as Record;
+ expect(body.source).toBe('deployment-status-file');
+ expect(body.complete).toBe(true);
+ expect(body.version).toBe('test-gru-pools');
+ expect(Array.isArray(body.pools)).toBe(true);
+ expect((body.pools as unknown[]).length).toBeGreaterThanOrEqual(1);
+ expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({
+ poolAddress: '0x1111111111111111111111111111111111111111',
+ section: 'pmmPools',
+ });
+ } finally {
+ await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined));
+ if (previousPath === undefined) {
+ delete process.env.DEPLOYMENT_STATUS_JSON_PATH;
+ } else {
+ process.env.DEPLOYMENT_STATUS_JSON_PATH = previousPath;
+ }
+ }
+ });
+ });
+
describe('GET /api/v1/report/gas-registry', () => {
it('reads the live gas rollout registry from deployment-status json when available', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/gas-registry?chainId=10`);
diff --git a/services/token-aggregation/src/api/routes/report.ts b/services/token-aggregation/src/api/routes/report.ts
index 9e6b238..4f06c95 100644
--- a/services/token-aggregation/src/api/routes/report.ts
+++ b/services/token-aggregation/src/api/routes/report.ts
@@ -26,6 +26,8 @@ import {
loadDeploymentStatusFile,
type CwRegistryChain,
} from '../../config/deployment-status';
+import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools';
+import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -94,6 +96,8 @@ async function buildTokenReport(chainId: number) {
})
);
+ const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address);
+
out.push({
chainId,
address: address.toLowerCase(),
@@ -110,7 +114,7 @@ async function buildTokenReport(chainId: number) {
liquiditySourceSymbol: spec.liquiditySourceSymbol,
market: marketData
? {
- priceUsd: marketData.priceUsd,
+ priceUsd: marketData.priceUsd ?? fallbackPriceUsd,
volume24h: marketData.volume24h,
volume7d: marketData.volume7d,
volume30d: marketData.volume30d,
@@ -118,6 +122,15 @@ async function buildTokenReport(chainId: number) {
liquidityUsd: marketData.liquidityUsd,
lastUpdated: marketData.lastUpdated?.toISOString() ?? '',
}
+ : fallbackPriceUsd !== undefined
+ ? {
+ priceUsd: fallbackPriceUsd,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`,
+ }
: undefined,
pools: resolvedPools.map((p) => ({
poolAddress: p.poolAddress,
@@ -543,6 +556,36 @@ router.get('/cw-registry', async (req: Request, res: Response) => {
}
});
+/** GET /report/gru-v2-pmm-pools — all GRU v2 PMM pools from deployment-status (stable, volatile, gas) with resolved token addresses. */
+router.get('/gru-v2-pmm-pools', async (req: Request, res: Response) => {
+ try {
+ const chainIdParam = req.query.chainId as string | undefined;
+ const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
+ const fileBacked = loadDeploymentStatusFile();
+ let pools = getGruV2DeploymentPoolRows();
+
+ if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
+ pools = pools.filter((p) => p.chainId === chainIdFilter);
+ }
+
+ res.set('Cache-Control', 'public, max-age=0, must-revalidate');
+ res.json({
+ generatedAt: new Date().toISOString(),
+ source: fileBacked ? 'deployment-status-file' : 'none',
+ complete: !!fileBacked,
+ version: fileBacked?.data.version,
+ updated: fileBacked?.data.updated,
+ lastModified: fileBacked?.lastModified,
+ homeChainId: fileBacked?.data.homeChainId,
+ count: pools.length,
+ pools,
+ });
+ } catch (error) {
+ logger.error('Error building report/gru-v2-pmm-pools:', error);
+ res.status(500).json({ error: 'Internal server error', pools: [] });
+ }
+});
+
/** GET /report/gas-registry — live gas-family rollout registry from deployment-status.json plus GRU transport metadata. */
router.get('/gas-registry', async (req: Request, res: Response) => {
try {
diff --git a/services/token-aggregation/src/api/routes/tokens.test.ts b/services/token-aggregation/src/api/routes/tokens.test.ts
new file mode 100644
index 0000000..1d25514
--- /dev/null
+++ b/services/token-aggregation/src/api/routes/tokens.test.ts
@@ -0,0 +1,219 @@
+import { createServer } from 'http';
+import express from 'express';
+import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
+
+const mockGetTokens = jest.fn();
+const mockGetToken = jest.fn();
+const mockSearchTokens = jest.fn();
+const mockGetMarketData = jest.fn();
+const mockGetPoolsByToken = jest.fn();
+const mockGetPool = jest.fn();
+const mockGetLiveDodoPools = jest.fn();
+const mockResolveTokenDisplay = jest.fn();
+const mockResolvePoolTokenDisplays = jest.fn();
+const mockGetTokenByContract = jest.fn();
+
+jest.mock('../../database/repositories/token-repo', () => ({
+ TokenRepository: jest.fn().mockImplementation(() => ({
+ getTokens: mockGetTokens,
+ getToken: mockGetToken,
+ searchTokens: mockSearchTokens,
+ })),
+}));
+
+jest.mock('../../database/repositories/market-data-repo', () => ({
+ MarketDataRepository: jest.fn().mockImplementation(() => ({
+ getMarketData: mockGetMarketData,
+ })),
+}));
+
+jest.mock('../../database/repositories/pool-repo', () => ({
+ PoolRepository: jest.fn().mockImplementation(() => ({
+ getPoolsByToken: mockGetPoolsByToken,
+ getPool: mockGetPool,
+ })),
+}));
+
+jest.mock('../../indexer/ohlcv-generator', () => ({
+ OHLCVGenerator: jest.fn().mockImplementation(() => ({
+ getOHLCV: jest.fn().mockResolvedValue([]),
+ })),
+}));
+
+const mockGetMarketDataAdapter = jest.fn();
+
+jest.mock('../../adapters/coingecko-adapter', () => ({
+ CoinGeckoAdapter: jest.fn().mockImplementation(() => ({
+ getTokenByContract: mockGetTokenByContract,
+ getMarketData: mockGetMarketDataAdapter,
+ getTrending: jest.fn().mockResolvedValue([]),
+ })),
+}));
+
+jest.mock('../../adapters/cmc-adapter', () => ({
+ CoinMarketCapAdapter: jest.fn().mockImplementation(() => ({
+ getTokenByContract: mockGetTokenByContract,
+ getMarketData: mockGetMarketDataAdapter,
+ })),
+}));
+
+jest.mock('../../adapters/dexscreener-adapter', () => ({
+ DexScreenerAdapter: jest.fn().mockImplementation(() => ({
+ getTokenByContract: mockGetTokenByContract,
+ getMarketData: mockGetMarketDataAdapter,
+ })),
+}));
+
+jest.mock('../../services/live-dodo-fallback', () => ({
+ getLiveDodoPools: (...args: unknown[]) => mockGetLiveDodoPools(...args),
+}));
+
+jest.mock('../../services/token-display', () => ({
+ resolveTokenDisplay: (...args: unknown[]) => mockResolveTokenDisplay(...args),
+ resolvePoolTokenDisplays: (...args: unknown[]) => mockResolvePoolTokenDisplays(...args),
+}));
+
+jest.mock('../middleware/cache');
+
+const tokensRoutes = require('./tokens').default as typeof import('./tokens').default;
+
+function createApp() {
+ const app = express();
+ app.use('/api/v1', tokensRoutes);
+ return app;
+}
+
+async function startServer(app: express.Application): Promise<{ server: ReturnType; baseUrl: string }> {
+ const server = createServer(app);
+ await new Promise((resolve) => server.listen(0, () => resolve()));
+ const port = (server.address() as { port: number }).port;
+ return { server, baseUrl: `http://127.0.0.1:${port}` };
+}
+
+describe('Tokens API', () => {
+ let server: ReturnType;
+ let baseUrl: string;
+
+ beforeAll(async () => {
+ const app = createApp();
+ const started = await startServer(app);
+ server = started.server;
+ baseUrl = started.baseUrl;
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockGetMarketDataAdapter.mockResolvedValue(null);
+ mockGetTokens.mockResolvedValue([]);
+ mockGetToken.mockResolvedValue(null);
+ mockSearchTokens.mockResolvedValue([]);
+ mockGetMarketData.mockResolvedValue(null);
+ mockGetPoolsByToken.mockResolvedValue([]);
+ mockGetPool.mockResolvedValue(null);
+ mockGetLiveDodoPools.mockResolvedValue([]);
+ mockResolveTokenDisplay.mockResolvedValue({
+ address: '',
+ name: 'Unknown Token',
+ symbol: 'UNKNOWN',
+ decimals: 18,
+ source: 'fallback',
+ });
+ mockResolvePoolTokenDisplays.mockResolvedValue({
+ token0: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
+ token1: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
+ });
+ mockGetTokenByContract.mockResolvedValue(null);
+ });
+
+ afterAll(async () => {
+ await new Promise((resolve, reject) => {
+ server.close((error) => {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve();
+ });
+ });
+ });
+
+ it('lists canonical 138 tokens with stable and ETH-family fallback pricing when db market data is missing', async () => {
+ const usdt = getCanonicalTokenBySymbol(138, 'USDT');
+ const weth = getCanonicalTokenBySymbol(138, 'WETH');
+ const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
+
+ expect(usdt?.addresses[138]).toBeTruthy();
+ expect(weth?.addresses[138]).toBeTruthy();
+ expect(weth10?.addresses[138]).toBeTruthy();
+
+ const res = await fetch(`${baseUrl}/api/v1/tokens?chainId=138&limit=400`);
+ expect(res.status).toBe(200);
+
+ const body = (await res.json()) as Record;
+ expect(body.source).toBe('canonical');
+
+ const findByAddress = (address?: string) =>
+ body.tokens.find((token: Record) => token.address === address?.toLowerCase());
+
+ expect(findByAddress(usdt?.addresses[138])).toMatchObject({
+ symbol: 'USDT',
+ decimals: 6,
+ market: expect.objectContaining({
+ priceUsd: 1,
+ volume24h: 0,
+ liquidityUsd: 0,
+ }),
+ });
+
+ expect(findByAddress(weth?.addresses[138])).toMatchObject({
+ symbol: 'WETH',
+ decimals: 18,
+ market: expect.objectContaining({
+ priceUsd: 2490,
+ }),
+ });
+
+ expect(findByAddress(weth10?.addresses[138])).toMatchObject({
+ symbol: 'WETH10',
+ decimals: 18,
+ market: expect.objectContaining({
+ priceUsd: 2490,
+ }),
+ });
+ });
+
+ it('fills missing priceUsd on token detail responses while preserving repository market fields', async () => {
+ const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
+ expect(weth10?.addresses[138]).toBeTruthy();
+ const weth10Address = String(weth10?.addresses[138]).toLowerCase();
+
+ mockGetMarketData.mockResolvedValue({
+ chainId: 138,
+ tokenAddress: weth10Address,
+ priceUsd: undefined,
+ volume24h: 1234,
+ volume7d: 5678,
+ volume30d: 9012,
+ liquidityUsd: 3456,
+ holdersCount: 78,
+ transfers24h: 9,
+ lastUpdated: new Date('2026-04-16T00:00:00.000Z'),
+ });
+
+ const res = await fetch(`${baseUrl}/api/v1/tokens/${weth10Address}?chainId=138`);
+ expect(res.status).toBe(200);
+
+ const body = (await res.json()) as Record;
+ expect(body.token).toMatchObject({
+ symbol: 'WETH10',
+ decimals: 18,
+ market: expect.objectContaining({
+ priceUsd: 2490,
+ volume24h: 1234,
+ liquidityUsd: 3456,
+ }),
+ hasDodoPool: false,
+ });
+ expect(body.token.canonicalLiquidity).toBeUndefined();
+ });
+});
diff --git a/services/token-aggregation/src/api/routes/tokens.ts b/services/token-aggregation/src/api/routes/tokens.ts
index c3f76a7..6fac348 100644
--- a/services/token-aggregation/src/api/routes/tokens.ts
+++ b/services/token-aggregation/src/api/routes/tokens.ts
@@ -16,6 +16,11 @@ import {
resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
+import {
+ buildExplorerLinks,
+ mergeMarketWithValuation,
+ resolveUsdValuation,
+} from '../../services/valuation-precedence';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -26,6 +31,26 @@ const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();
+function buildMarketPricingExplorer(
+ chainId: number,
+ displayAddress: string,
+ lookupAddress: string,
+ marketData: Awaited>,
+ external: { coingecko?: Awaited>; cmc?: Awaited>; dexscreener?: Awaited> } | null
+) {
+ const pricing = resolveUsdValuation({
+ chainId,
+ normalizedAddress: lookupAddress.toLowerCase(),
+ indexer: marketData,
+ coingecko: external?.coingecko ?? undefined,
+ cmc: external?.cmc ?? undefined,
+ dexscreener: external?.dexscreener ?? undefined,
+ });
+ const market = mergeMarketWithValuation(chainId, displayAddress.toLowerCase(), marketData, pricing);
+ const explorer = buildExplorerLinks(chainId, displayAddress);
+ return { market, pricing, explorer };
+}
+
function tokenFromCanonical(chainId: number, address: string): Token | null {
const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase());
if (!spec) {
@@ -182,10 +207,20 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
const { tokens, source } = await getTokensWithFallback(chainId, limit, offset);
const tokensWithMarketData = await Promise.all(
tokens.map(async (token) => {
- const marketData = await marketDataRepo.getMarketData(chainId, token.address);
+ const resolution = resolveCanonicalQuoteAddress(chainId, token.address);
+ const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
+ const { market, pricing, explorer } = buildMarketPricingExplorer(
+ chainId,
+ token.address,
+ resolution.lookupAddress,
+ marketData,
+ null
+ );
const out: Record = {
...token,
- market: marketData || undefined,
+ market: market || undefined,
+ pricing,
+ explorer,
};
if (includeDodoPool) {
const pools = await getPoolsByTokenWithFallback(chainId, token.address);
@@ -228,13 +263,32 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
return res.status(404).json({ error: 'Token not found' });
}
- const [marketData, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([
+ const [
+ marketDataRaw,
+ pools,
+ coingeckoData,
+ cmcData,
+ dexscreenerData,
+ coingeckoMarket,
+ cmcMarket,
+ dexscreenerMarket,
+ ] = await Promise.all([
marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
getPoolsByTokenWithFallback(chainId, normalizedAddress),
coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress),
cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress),
dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress),
+ coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress),
+ cmcAdapter.getMarketData(chainId, resolution.lookupAddress),
+ dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress),
]);
+ const { market: marketData, pricing, explorer } = buildMarketPricingExplorer(
+ chainId,
+ normalizedAddress,
+ resolution.lookupAddress,
+ marketDataRaw,
+ { coingecko: coingeckoMarket, cmc: cmcMarket, dexscreener: dexscreenerMarket }
+ );
res.json({
token: {
@@ -243,6 +297,8 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
totalSupply: token.totalSupply,
},
market: marketData || undefined,
+ pricing,
+ explorer,
external: {
coingecko: coingeckoData || undefined,
cmc: cmcData || undefined,
diff --git a/services/token-aggregation/src/config/canonical-tokens.test.ts b/services/token-aggregation/src/config/canonical-tokens.test.ts
index 8b13402..60f8c59 100644
--- a/services/token-aggregation/src/config/canonical-tokens.test.ts
+++ b/services/token-aggregation/src/config/canonical-tokens.test.ts
@@ -115,6 +115,26 @@ describe('canonical cW token catalog', () => {
expect(cwethL2?.addresses[10]).toBe('0xce7200000000000000000000000000000000000a');
expect(getCanonicalTokenByAddress(10, '0xce7200000000000000000000000000000000000a')?.symbol).toBe('cWETHL2');
expect(getTokenRegistryFamily(cwethL2!)).toBe('gas_native');
+
+ const weth = getCanonicalTokenBySymbol(138, 'WETH');
+ expect(weth).toMatchObject({
+ symbol: 'WETH',
+ type: 'w',
+ currencyCode: 'ETH',
+ decimals: 18,
+ });
+ expect(weth?.addresses[138]).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
+ expect(getCanonicalTokenByAddress(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')?.symbol).toBe('WETH');
+
+ const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
+ expect(weth10).toMatchObject({
+ symbol: 'WETH10',
+ type: 'w',
+ currencyCode: 'ETH',
+ decimals: 18,
+ });
+ expect(weth10?.addresses[138]).toBe('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f');
+ expect(getCanonicalTokenByAddress(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')?.symbol).toBe('WETH10');
});
it('surfaces cAUSDT on Chain 138 from env and keeps cWAUSDT fallback mirrors on active public chains', () => {
diff --git a/services/token-aggregation/src/config/canonical-tokens.ts b/services/token-aggregation/src/config/canonical-tokens.ts
index cf73245..61796d1 100644
--- a/services/token-aggregation/src/config/canonical-tokens.ts
+++ b/services/token-aggregation/src/config/canonical-tokens.ts
@@ -247,6 +247,8 @@ const FALLBACK_ADDRESSES: Record>> = {
cCADC: { [CHAIN_138]: '0x54dBd40cF05e15906A2C21f600937e96787f5679' },
cXAUC: { [CHAIN_138]: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' },
cXAUT: { [CHAIN_138]: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' },
+ WETH: { [CHAIN_138]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
+ WETH10: { [CHAIN_138]: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' },
// ISO-4217W on Cronos (25) — from DeployISO4217WSystem
USDW: { [CHAIN_25]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036' },
EURW: { [CHAIN_25]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd' },
@@ -450,6 +452,26 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
{ symbol: 'cWUSDC', name: 'USD Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDC', id)])) } },
{ symbol: 'cWUSDT', name: 'Tether USD (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDT', id)])) } },
{ symbol: 'cWUSDW', name: 'USD W (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDW.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDW', id)])) } },
+ {
+ symbol: 'WETH',
+ name: 'Wrapped Ether (WETH9)',
+ type: 'w',
+ decimals: 18,
+ currencyCode: 'ETH',
+ registryFamily: 'gas_native',
+ description: 'Legacy WETH9 surface used on Chain 138 for canonical ETH swap routing and CCIP WETH9 bridge lanes.',
+ addresses: { [CHAIN_138]: addr('WETH', CHAIN_138) || '' },
+ },
+ {
+ symbol: 'WETH10',
+ name: 'Wrapped Ether 10',
+ type: 'w',
+ decimals: 18,
+ currencyCode: 'ETH',
+ registryFamily: 'gas_native',
+ description: 'Chain 138 WETH10 pilot wrapped ETH surface used by DODO v3 routing and flash-capable paths.',
+ addresses: { [CHAIN_138]: addr('WETH10', CHAIN_138) || '' },
+ },
{
symbol: 'cWBTC',
name: 'Bitcoin (Compliant Wrapped Monetary Unit)',
@@ -728,6 +750,8 @@ const LOGO_BY_SYMBOL: Record = {
cWUSDC: USDC_LOGO,
cWUSDT: USDT_LOGO,
cWUSDW: USDC_LOGO,
+ WETH: ETH_LOGO,
+ WETH10: ETH_LOGO,
cEURC: `${GRU_LOGO_BASE}/cEURC.svg`,
cEURT: `${GRU_LOGO_BASE}/cEURT.svg`,
cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`,
diff --git a/services/token-aggregation/src/config/dex-factories.ts b/services/token-aggregation/src/config/dex-factories.ts
index 8d41a82..55bb618 100644
--- a/services/token-aggregation/src/config/dex-factories.ts
+++ b/services/token-aggregation/src/config/dex-factories.ts
@@ -1,4 +1,4 @@
-export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
+export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
export interface UniswapV2Config {
factory: string;
@@ -30,6 +30,7 @@ export interface CustomDexConfig {
export interface DexFactoryConfig {
uniswap_v2?: UniswapV2Config[];
uniswap_v3?: UniswapV3Config[];
+ sushiswap?: UniswapV2Config[];
dodo?: DodoConfig[];
custom?: CustomDexConfig[];
}
@@ -38,6 +39,35 @@ export interface DexFactoryConfig {
const CANONICAL_CHAIN138_DODO_PMM_INTEGRATION =
'0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';
+function getUniswapV2Config(chainId: number): UniswapV2Config[] | undefined {
+ const factory = process.env[`CHAIN_${chainId}_UNISWAP_V2_FACTORY`];
+ if (!factory) return undefined;
+
+ return [
+ {
+ factory,
+ router: process.env[`CHAIN_${chainId}_UNISWAP_V2_ROUTER`] || '',
+ startBlock: parseInt(process.env[`CHAIN_${chainId}_UNISWAP_V2_START_BLOCK`] || '0', 10),
+ },
+ ];
+}
+
+function getDodoConfig(chainId: number): DodoConfig[] | undefined {
+ const poolManager = process.env[`CHAIN_${chainId}_DODO_POOL_MANAGER`] || '';
+ const dodoPmmIntegration = process.env[`CHAIN_${chainId}_DODO_PMM_INTEGRATION`] || '';
+
+ if (!poolManager && !dodoPmmIntegration) return undefined;
+
+ return [
+ {
+ poolManager,
+ dodoPmmIntegration,
+ dodoVendingMachine: process.env[`CHAIN_${chainId}_DODO_VENDING_MACHINE`] || '',
+ startBlock: parseInt(process.env[`CHAIN_${chainId}_DODO_START_BLOCK`] || '0', 10),
+ },
+ ];
+}
+
export const DEX_FACTORIES: Record = {
138: {
// DODO PMM Integration - index from DODOPMMIntegration or PoolManager
@@ -51,12 +81,13 @@ export const DEX_FACTORIES: Record = {
},
],
// UniswapV2 - if deployed
- uniswap_v2: process.env.CHAIN_138_UNISWAP_V2_FACTORY
+ uniswap_v2: getUniswapV2Config(138),
+ sushiswap: process.env.CHAIN_138_SUSHISWAP_FACTORY
? [
{
- factory: process.env.CHAIN_138_UNISWAP_V2_FACTORY,
- router: process.env.CHAIN_138_UNISWAP_V2_ROUTER || '',
- startBlock: parseInt(process.env.CHAIN_138_UNISWAP_V2_START_BLOCK || '0', 10),
+ factory: process.env.CHAIN_138_SUSHISWAP_FACTORY,
+ router: process.env.CHAIN_138_SUSHISWAP_ROUTER || '',
+ startBlock: parseInt(process.env.CHAIN_138_SUSHISWAP_START_BLOCK || '0', 10),
},
]
: undefined,
@@ -74,15 +105,7 @@ export const DEX_FACTORIES: Record = {
651940: {
// ALL Mainnet - DEX factories to be discovered/configured
// These can be set via environment variables or discovered on-chain
- uniswap_v2: process.env.CHAIN_651940_UNISWAP_V2_FACTORY
- ? [
- {
- factory: process.env.CHAIN_651940_UNISWAP_V2_FACTORY,
- router: process.env.CHAIN_651940_UNISWAP_V2_ROUTER || '',
- startBlock: parseInt(process.env.CHAIN_651940_UNISWAP_V2_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(651940),
uniswap_v3: process.env.CHAIN_651940_UNISWAP_V3_FACTORY
? [
{
@@ -101,72 +124,61 @@ export const DEX_FACTORIES: Record = {
},
]
: undefined,
+ custom: process.env.CHAIN_651940_HYDX_FACTORY
+ ? [
+ {
+ factory: process.env.CHAIN_651940_HYDX_FACTORY,
+ router: process.env.CHAIN_651940_HYDX_ROUTER || '',
+ startBlock: parseInt(process.env.CHAIN_651940_HYDX_START_BLOCK || '0', 10),
+ pairCreatedEvent: process.env.CHAIN_651940_HYDX_PAIR_CREATED_EVENT || '',
+ },
+ ]
+ : undefined,
},
// cW* edge chains (1, 10, 56, 100, 137): set CHAIN_*_DODO_PMM_INTEGRATION or CHAIN_*_DODO_POOL_MANAGER to index DODO/pools
1: {
- dodo:
- process.env.CHAIN_1_DODO_PMM_INTEGRATION || process.env.CHAIN_1_DODO_POOL_MANAGER
- ? [
- {
- poolManager: process.env.CHAIN_1_DODO_POOL_MANAGER || '',
- dodoPmmIntegration: process.env.CHAIN_1_DODO_PMM_INTEGRATION || '',
- dodoVendingMachine: process.env.CHAIN_1_DODO_VENDING_MACHINE || '',
- startBlock: parseInt(process.env.CHAIN_1_DODO_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(1),
+ dodo: getDodoConfig(1),
},
10: {
- dodo:
- process.env.CHAIN_10_DODO_PMM_INTEGRATION || process.env.CHAIN_10_DODO_POOL_MANAGER
- ? [
- {
- poolManager: process.env.CHAIN_10_DODO_POOL_MANAGER || '',
- dodoPmmIntegration: process.env.CHAIN_10_DODO_PMM_INTEGRATION || '',
- dodoVendingMachine: process.env.CHAIN_10_DODO_VENDING_MACHINE || '',
- startBlock: parseInt(process.env.CHAIN_10_DODO_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(10),
+ dodo: getDodoConfig(10),
+ },
+ 25: {
+ uniswap_v2: getUniswapV2Config(25),
+ dodo: getDodoConfig(25),
},
56: {
- dodo:
- process.env.CHAIN_56_DODO_PMM_INTEGRATION || process.env.CHAIN_56_DODO_POOL_MANAGER
- ? [
- {
- poolManager: process.env.CHAIN_56_DODO_POOL_MANAGER || '',
- dodoPmmIntegration: process.env.CHAIN_56_DODO_PMM_INTEGRATION || '',
- dodoVendingMachine: process.env.CHAIN_56_DODO_VENDING_MACHINE || '',
- startBlock: parseInt(process.env.CHAIN_56_DODO_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(56),
+ dodo: getDodoConfig(56),
},
100: {
- dodo:
- process.env.CHAIN_100_DODO_PMM_INTEGRATION || process.env.CHAIN_100_DODO_POOL_MANAGER
- ? [
- {
- poolManager: process.env.CHAIN_100_DODO_POOL_MANAGER || '',
- dodoPmmIntegration: process.env.CHAIN_100_DODO_PMM_INTEGRATION || '',
- dodoVendingMachine: process.env.CHAIN_100_DODO_VENDING_MACHINE || '',
- startBlock: parseInt(process.env.CHAIN_100_DODO_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(100),
+ dodo: getDodoConfig(100),
},
137: {
- dodo:
- process.env.CHAIN_137_DODO_PMM_INTEGRATION || process.env.CHAIN_137_DODO_POOL_MANAGER
- ? [
- {
- poolManager: process.env.CHAIN_137_DODO_POOL_MANAGER || '',
- dodoPmmIntegration: process.env.CHAIN_137_DODO_PMM_INTEGRATION || '',
- dodoVendingMachine: process.env.CHAIN_137_DODO_VENDING_MACHINE || '',
- startBlock: parseInt(process.env.CHAIN_137_DODO_START_BLOCK || '0', 10),
- },
- ]
- : undefined,
+ uniswap_v2: getUniswapV2Config(137),
+ dodo: getDodoConfig(137),
+ },
+ 8453: {
+ uniswap_v2: getUniswapV2Config(8453),
+ dodo: getDodoConfig(8453),
+ },
+ 42161: {
+ uniswap_v2: getUniswapV2Config(42161),
+ dodo: getDodoConfig(42161),
+ },
+ 42220: {
+ uniswap_v2: getUniswapV2Config(42220),
+ dodo: getDodoConfig(42220),
+ },
+ 43114: {
+ uniswap_v2: getUniswapV2Config(43114),
+ dodo: getDodoConfig(43114),
+ },
+ 1111: {
+ uniswap_v2: getUniswapV2Config(1111),
+ dodo: getDodoConfig(1111),
},
};
@@ -189,6 +201,8 @@ export function hasDexType(chainId: number, dexType: DexType): boolean {
return !!config.uniswap_v2 && config.uniswap_v2.length > 0;
case 'uniswap_v3':
return !!config.uniswap_v3 && config.uniswap_v3.length > 0;
+ case 'sushiswap':
+ return !!config.sushiswap && config.sushiswap.length > 0;
case 'dodo':
return !!config.dodo && config.dodo.length > 0;
case 'custom':
@@ -208,6 +222,7 @@ export function getConfiguredDexTypes(chainId: number): DexType[] {
const types: DexType[] = [];
if (hasDexType(chainId, 'uniswap_v2')) types.push('uniswap_v2');
if (hasDexType(chainId, 'uniswap_v3')) types.push('uniswap_v3');
+ if (hasDexType(chainId, 'sushiswap')) types.push('sushiswap');
if (hasDexType(chainId, 'dodo')) types.push('dodo');
if (hasDexType(chainId, 'custom')) types.push('custom');
diff --git a/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts b/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts
new file mode 100644
index 0000000..842f707
--- /dev/null
+++ b/services/token-aggregation/src/config/gru-v2-deployment-pools.test.ts
@@ -0,0 +1,83 @@
+import type { DeploymentStatusFile } from './deployment-status';
+import {
+ buildGruV2PoolRegistryFromDeploymentData,
+ preferGruV2OfficialDexPairs,
+ resolveDeploymentTokenAddress,
+} from './gru-v2-deployment-pools';
+
+describe('gru-v2-deployment-pools', () => {
+ it('resolveDeploymentTokenAddress checks cwTokens, gasMirrors, anchors, gasQuoteAddresses', () => {
+ const chain = {
+ cwTokens: { cWUSDT: '0xcc' },
+ anchorAddresses: { USDC: '0xaa' },
+ gasQuoteAddresses: { WETH: '0xee' },
+ };
+ expect(resolveDeploymentTokenAddress(chain, 'cWUSDT')).toBe('0xcc');
+ expect(resolveDeploymentTokenAddress(chain, 'USDC')).toBe('0xaa');
+ expect(resolveDeploymentTokenAddress(chain, 'WETH')).toBe('0xee');
+ expect(resolveDeploymentTokenAddress(chain, 'MISSING')).toBeNull();
+ });
+
+ it('buildGruV2PoolRegistryFromDeploymentData merges pmmPools, pmmPoolsVolatile, and gasPmmPools', () => {
+ const data = {
+ chains: {
+ '1': {
+ name: 'Ethereum Mainnet',
+ cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' },
+ anchorAddresses: {
+ USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ TRUU: '0xdae0fafd65385e7775cf75b1398735155ef6acd2',
+ },
+ pmmPools: [
+ {
+ base: 'cWUSDT',
+ quote: 'USDC',
+ poolAddress: '0x1111111111111111111111111111111111111111',
+ feeBps: 3,
+ role: 'public_routing',
+ publicRoutingEnabled: true,
+ },
+ ],
+ pmmPoolsVolatile: [
+ {
+ base: 'cWUSDT',
+ quote: 'TRUU',
+ poolAddress: '0x2222222222222222222222222222222222222222',
+ feeBps: 30,
+ role: 'truu_routing',
+ },
+ ],
+ gasMirrors: { cWETH: '0xf6dc5587e18f27adff60e303fdd98f35b50fa8a5' },
+ gasQuoteAddresses: {
+ WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
+ USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ },
+ gasPmmPools: [
+ {
+ familyKey: 'eth_mainnet',
+ base: 'cWETH',
+ quote: 'USDC',
+ poolAddress: '0x3333333333333333333333333333333333333333',
+ feeBps: 30,
+ role: 'public_routing',
+ venue: 'dodo_pmm',
+ },
+ ],
+ },
+ },
+ } as unknown as DeploymentStatusFile;
+
+ const { rows, byChainTokenPools } = buildGruV2PoolRegistryFromDeploymentData(data);
+ expect(rows).toHaveLength(3);
+ expect(rows.map((r) => r.section).sort()).toEqual(['gasPmmPools', 'pmmPools', 'pmmPoolsVolatile']);
+
+ const usdt = '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae';
+ expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x1111111111111111111111111111111111111111')).toBe(true);
+ expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x2222222222222222222222222222222222222222')).toBe(true);
+ });
+
+ it('preferGruV2OfficialDexPairs leaves pairs unchanged when no deployment pools index the token', () => {
+ const pairs = [{ pairAddress: '0xbb', priceUsd: '1' }];
+ expect(preferGruV2OfficialDexPairs(99999, '0x0000000000000000000000000000000000000001', pairs)).toEqual(pairs);
+ });
+});
diff --git a/services/token-aggregation/src/config/gru-v2-deployment-pools.ts b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts
new file mode 100644
index 0000000..95ed486
--- /dev/null
+++ b/services/token-aggregation/src/config/gru-v2-deployment-pools.ts
@@ -0,0 +1,197 @@
+import type { DeploymentStatusFile } from './deployment-status';
+import { loadDeploymentStatusFile, resolveDeploymentStatusPath } from './deployment-status';
+
+export type GruV2PmmSection = 'pmmPools' | 'pmmPoolsVolatile' | 'gasPmmPools';
+
+export interface GruV2DeploymentPoolRow {
+ chainId: number;
+ chainName: string;
+ section: GruV2PmmSection;
+ baseSymbol: string;
+ quoteSymbol: string;
+ baseAddress: string;
+ quoteAddress: string;
+ poolAddress: string;
+ feeBps?: number;
+ role?: string;
+ publicRoutingEnabled?: boolean;
+ familyKey?: string;
+ venue?: string;
+}
+
+interface ChainTokenMaps {
+ cwTokens?: Record;
+ gasMirrors?: Record;
+ anchorAddresses?: Record;
+ gasQuoteAddresses?: Record;
+}
+
+/** Resolve a pool leg symbol to an address using deployment-status chain maps (cW*, anchors, gas quotes). */
+export function resolveDeploymentTokenAddress(chain: ChainTokenMaps, symbol: string): string | null {
+ const candidates = [
+ chain.cwTokens?.[symbol],
+ chain.gasMirrors?.[symbol],
+ chain.anchorAddresses?.[symbol],
+ chain.gasQuoteAddresses?.[symbol],
+ ];
+ for (const a of candidates) {
+ if (typeof a === 'string' && a.startsWith('0x')) {
+ return a.toLowerCase();
+ }
+ }
+ return null;
+}
+
+function pushTokenPoolIndex(
+ byChainTokenPools: Map>,
+ chainId: number,
+ tokenAddress: string,
+ poolAddress: string
+): void {
+ const key = `${chainId}:${tokenAddress.toLowerCase()}`;
+ let set = byChainTokenPools.get(key);
+ if (!set) {
+ set = new Set();
+ byChainTokenPools.set(key, set);
+ }
+ set.add(poolAddress.toLowerCase());
+}
+
+/**
+ * Build GRU v2 PMM pool rows and a (chainId:token) → official pool addresses index from deployment-status data.
+ * Includes stable mesh (`pmmPools`), volatile (`pmmPoolsVolatile`), and gas (`gasPmmPools`) sections.
+ */
+export function buildGruV2PoolRegistryFromDeploymentData(data: DeploymentStatusFile): {
+ rows: GruV2DeploymentPoolRow[];
+ byChainTokenPools: Map>;
+} {
+ const rows: GruV2DeploymentPoolRow[] = [];
+ const byChainTokenPools = new Map>();
+
+ const sections: GruV2PmmSection[] = ['pmmPools', 'pmmPoolsVolatile', 'gasPmmPools'];
+
+ for (const [cid, rawChain] of Object.entries(data.chains ?? {})) {
+ const chainId = Number(cid);
+ if (Number.isNaN(chainId)) continue;
+ const chain = rawChain as ChainTokenMaps & {
+ name?: string;
+ pmmPools?: unknown;
+ pmmPoolsVolatile?: unknown;
+ gasPmmPools?: unknown;
+ };
+ const chainName = typeof chain.name === 'string' ? chain.name : `Chain ${cid}`;
+
+ for (const section of sections) {
+ const arr = chain[section];
+ if (!Array.isArray(arr)) continue;
+
+ for (const raw of arr) {
+ if (!raw || typeof raw !== 'object') continue;
+ const pool = raw as Record;
+ const poolAddress = typeof pool.poolAddress === 'string' ? pool.poolAddress.trim() : '';
+ const baseSymbol = typeof pool.base === 'string' ? pool.base : '';
+ const quoteSymbol = typeof pool.quote === 'string' ? pool.quote : '';
+ if (!poolAddress.startsWith('0x') || !baseSymbol || !quoteSymbol) continue;
+
+ const baseAddress = resolveDeploymentTokenAddress(chain, baseSymbol);
+ const quoteAddress = resolveDeploymentTokenAddress(chain, quoteSymbol);
+ if (!baseAddress || !quoteAddress) continue;
+
+ const row: GruV2DeploymentPoolRow = {
+ chainId,
+ chainName,
+ section,
+ baseSymbol,
+ quoteSymbol,
+ baseAddress,
+ quoteAddress,
+ poolAddress: poolAddress.toLowerCase(),
+ feeBps: typeof pool.feeBps === 'number' ? pool.feeBps : undefined,
+ role: typeof pool.role === 'string' ? pool.role : undefined,
+ publicRoutingEnabled:
+ typeof pool.publicRoutingEnabled === 'boolean' ? pool.publicRoutingEnabled : undefined,
+ familyKey: typeof pool.familyKey === 'string' ? pool.familyKey : undefined,
+ venue: typeof pool.venue === 'string' ? pool.venue : undefined,
+ };
+ rows.push(row);
+
+ pushTokenPoolIndex(byChainTokenPools, chainId, baseAddress, poolAddress);
+ pushTokenPoolIndex(byChainTokenPools, chainId, quoteAddress, poolAddress);
+ }
+ }
+ }
+
+ rows.sort((a, b) => {
+ const c = a.chainId - b.chainId;
+ if (c !== 0) return c;
+ return a.poolAddress.localeCompare(b.poolAddress);
+ });
+
+ return { rows, byChainTokenPools };
+}
+
+let cachedSnapshot: {
+ sourcePath: string;
+ lastModified: string;
+ rows: GruV2DeploymentPoolRow[];
+ byChainTokenPools: Map>;
+} | null = null;
+
+function ensureRegistry(): void {
+ const loaded = loadDeploymentStatusFile();
+ const sourcePath = resolveDeploymentStatusPath() ?? '';
+ const lm = loaded?.lastModified ?? '';
+ if (cachedSnapshot && cachedSnapshot.lastModified === lm && cachedSnapshot.sourcePath === sourcePath) {
+ return;
+ }
+ if (!loaded) {
+ cachedSnapshot = { sourcePath, lastModified: lm, rows: [], byChainTokenPools: new Map() };
+ return;
+ }
+ const built = buildGruV2PoolRegistryFromDeploymentData(loaded.data);
+ cachedSnapshot = {
+ sourcePath,
+ lastModified: lm,
+ rows: built.rows,
+ byChainTokenPools: built.byChainTokenPools,
+ };
+}
+
+/** All resolved GRU v2 PMM pools from `deployment-status.json` (when available). */
+export function getGruV2DeploymentPoolRows(): GruV2DeploymentPoolRow[] {
+ ensureRegistry();
+ return cachedSnapshot?.rows ?? [];
+}
+
+/**
+ * Official pool contract addresses for this token on this chain (from deployment-status).
+ * Returns `null` when the registry is empty or this token has no GRU pools — callers should use all DexScreener pairs.
+ */
+export function getOfficialGruV2PoolAddressesForToken(chainId: number, normalizedTokenAddress: string): Set | null {
+ ensureRegistry();
+ if (!cachedSnapshot || cachedSnapshot.rows.length === 0) {
+ return null;
+ }
+ const key = `${chainId}:${normalizedTokenAddress.toLowerCase()}`;
+ const set = cachedSnapshot.byChainTokenPools.get(key);
+ if (!set || set.size === 0) {
+ return null;
+ }
+ return set;
+}
+
+/**
+ * When DexScreener returns multiple pairs, prefer rows whose `pairAddress` is an official GRU v2 pool; if none match, keep full list.
+ */
+export function preferGruV2OfficialDexPairs(
+ chainId: number,
+ tokenAddress: string,
+ pairs: T[]
+): T[] {
+ const official = getOfficialGruV2PoolAddressesForToken(chainId, tokenAddress.toLowerCase());
+ if (!official || official.size === 0) {
+ return pairs;
+ }
+ const preferred = pairs.filter((p) => official.has(p.pairAddress.toLowerCase()));
+ return preferred.length > 0 ? preferred : pairs;
+}
diff --git a/services/token-aggregation/src/config/provider-capabilities.ts b/services/token-aggregation/src/config/provider-capabilities.ts
index 764f9ab..5b58be6 100644
--- a/services/token-aggregation/src/config/provider-capabilities.ts
+++ b/services/token-aggregation/src/config/provider-capabilities.ts
@@ -94,6 +94,10 @@ function encodeOneInchRoute(router: string): string {
return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']);
}
+function encodeRouterV2Route(factory: string, router: string): string {
+ return abiCoder.encode(['address', 'address'], [factory, router]);
+}
+
function chain138DodoCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const dodoProvider =
@@ -384,6 +388,140 @@ function chain138UniswapCapabilities(): ProviderCapabilityRecord {
};
}
+function chain138UniswapV2Capabilities(): ProviderCapabilityRecord {
+ const assets = getChain138RoutingAssets();
+ const factory = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_FACTORY);
+ const router = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_ROUTER);
+ const wethUsdtPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDT_PAIR);
+ const wethUsdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDC_PAIR);
+ const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_CUSDT_CUSDC_PAIR);
+ const status = factory && router ? 'live' : 'planned';
+
+ const pairs = [
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'uniswap_v2',
+ tokenASymbol: 'WETH',
+ tokenAAddress: assets.WETH.address,
+ tokenBSymbol: 'USDT',
+ tokenBAddress: assets.USDT.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDT venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
+ }),
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'uniswap_v2',
+ tokenASymbol: 'WETH',
+ tokenAAddress: assets.WETH.address,
+ tokenBSymbol: 'USDC',
+ tokenBAddress: assets.USDC.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDC venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
+ }),
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'uniswap_v2',
+ tokenASymbol: 'cUSDT',
+ tokenAAddress: assets.cUSDT.address,
+ tokenBSymbol: 'cUSDC',
+ tokenBAddress: assets.cUSDC.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native Uniswap v2 GRU stable venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
+ }),
+ ];
+
+ return {
+ chainId: CHAIN_138,
+ provider: 'uniswap_v2',
+ executionMode: 'onchain',
+ live: status === 'live',
+ quoteLive: status === 'live',
+ executionLive: status === 'live',
+ supportedLegTypes: ['swap'],
+ pairs,
+ notes: ['Canonical Chain 138 native Uniswap v2 router/factory path.'],
+ };
+}
+
+function chain138SushiswapCapabilities(): ProviderCapabilityRecord {
+ const assets = getChain138RoutingAssets();
+ const factory = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_FACTORY);
+ const router = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_ROUTER);
+ const wethUsdtPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDT_PAIR);
+ const wethUsdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDC_PAIR);
+ const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_CUSDT_CUSDC_PAIR);
+ const status = factory && router ? 'live' : 'planned';
+
+ const pairs = [
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'sushiswap',
+ tokenASymbol: 'WETH',
+ tokenAAddress: assets.WETH.address,
+ tokenBSymbol: 'USDT',
+ tokenBAddress: assets.USDT.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDT venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
+ }),
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'sushiswap',
+ tokenASymbol: 'WETH',
+ tokenAAddress: assets.WETH.address,
+ tokenBSymbol: 'USDC',
+ tokenBAddress: assets.USDC.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDC venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
+ }),
+ ...bidirectionalPair({
+ chainId: CHAIN_138,
+ provider: 'sushiswap',
+ tokenASymbol: 'cUSDT',
+ tokenAAddress: assets.cUSDT.address,
+ tokenBSymbol: 'cUSDC',
+ tokenBAddress: assets.cUSDC.address,
+ status,
+ target: router,
+ providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined,
+ providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
+ notes: ['Canonical Chain 138 native SushiSwap-compatible GRU stable venue.'],
+ reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
+ }),
+ ];
+
+ return {
+ chainId: CHAIN_138,
+ provider: 'sushiswap',
+ executionMode: 'onchain',
+ live: status === 'live',
+ quoteLive: status === 'live',
+ executionLive: status === 'live',
+ supportedLegTypes: ['swap'],
+ pairs,
+ notes: ['Canonical Chain 138 native SushiSwap-compatible router/factory path.'],
+ };
+}
+
function chain138BalancerCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const vault = normalizeAddress(process.env.BALANCER_VAULT || CHAIN138_PILOT_BALANCER_VAULT);
@@ -538,6 +676,8 @@ export function getProviderCapabilities(chainId: number): ProviderCapabilityReco
chain138DodoCapabilities(),
chain138DodoV3Capabilities(),
chain138UniswapCapabilities(),
+ chain138UniswapV2Capabilities(),
+ chain138SushiswapCapabilities(),
chain138BalancerCapabilities(),
chain138CurveCapabilities(),
chain138OneInchCapabilities(),
diff --git a/services/token-aggregation/src/config/routing-policies.ts b/services/token-aggregation/src/config/routing-policies.ts
index e2e5833..db9660f 100644
--- a/services/token-aggregation/src/config/routing-policies.ts
+++ b/services/token-aggregation/src/config/routing-policies.ts
@@ -16,7 +16,7 @@ export function resolveRoutingPolicy(
const baseStandard: RoutingPolicy = {
profile: 'standard',
- allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch'],
+ allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch'],
defaultIntermediateAddresses: defaultIntermediates,
allowBridge: constraints.allowBridge !== false,
allowedBridgeLabels: ['GRUTransport', 'CCIPStableBridge', 'CCIPWETH9Bridge', 'UniversalCCIPBridge', 'AlltraAdapter'],
diff --git a/services/token-aggregation/src/database/repositories/admin-repo.ts b/services/token-aggregation/src/database/repositories/admin-repo.ts
index 17ef5b4..067977c 100644
--- a/services/token-aggregation/src/database/repositories/admin-repo.ts
+++ b/services/token-aggregation/src/database/repositories/admin-repo.ts
@@ -41,7 +41,7 @@ export interface ApiEndpoint {
export interface DexFactoryConfig {
id?: number;
chainId: number;
- dexType: 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
+ dexType: 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
factoryAddress: string;
routerAddress?: string;
poolManagerAddress?: string;
diff --git a/services/token-aggregation/src/database/repositories/pool-repo.ts b/services/token-aggregation/src/database/repositories/pool-repo.ts
index 9c2d8b1..4c5a28b 100644
--- a/services/token-aggregation/src/database/repositories/pool-repo.ts
+++ b/services/token-aggregation/src/database/repositories/pool-repo.ts
@@ -1,7 +1,7 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
-export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
+export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
export interface LiquidityPool {
id?: number;
diff --git a/services/token-aggregation/src/indexer/chain-indexer.ts b/services/token-aggregation/src/indexer/chain-indexer.ts
index a46a310..dbcad4e 100644
--- a/services/token-aggregation/src/indexer/chain-indexer.ts
+++ b/services/token-aggregation/src/indexer/chain-indexer.ts
@@ -9,6 +9,8 @@ import { CoinGeckoAdapter } from '../adapters/coingecko-adapter';
import { CoinMarketCapAdapter } from '../adapters/cmc-adapter';
import { DexScreenerAdapter } from '../adapters/dexscreener-adapter';
import { logger } from '../utils/logger';
+import { getCanonicalPriceUsd } from '../services/canonical-price-oracle';
+import { pickExternalMarketDataForIndexer } from '../services/valuation-precedence';
export class ChainIndexer {
private chainId: number;
@@ -153,8 +155,12 @@ export class ChainIndexer {
this.adapters.dexscreener.getMarketData(this.chainId, tokenAddress),
]);
- // Merge external data (prefer CoinGecko, fallback to others)
- const externalData = coingeckoData || dexscreenerData || cmcData;
+ const externalData = pickExternalMarketDataForIndexer(this.chainId, tokenAddress.toLowerCase(), {
+ coingecko: coingeckoData,
+ cmc: cmcData,
+ dexscreener: dexscreenerData,
+ });
+ const canonicalPriceUsd = getCanonicalPriceUsd(this.chainId, tokenAddress);
// Get pools for liquidity calculation
const tokenPools = pools.filter(
@@ -166,7 +172,7 @@ export class ChainIndexer {
await this.marketDataRepo.upsertMarketData({
chainId: this.chainId,
tokenAddress,
- priceUsd: externalData?.priceUsd,
+ priceUsd: externalData?.priceUsd ?? canonicalPriceUsd,
priceChange24h: externalData?.priceChange24h,
volume24h: volumeMetrics.volume24h || externalData?.volume24h || 0,
volume7d: volumeMetrics.volume7d,
diff --git a/services/token-aggregation/src/indexer/pool-indexer.ts b/services/token-aggregation/src/indexer/pool-indexer.ts
index e3847be..3da2b3e 100644
--- a/services/token-aggregation/src/indexer/pool-indexer.ts
+++ b/services/token-aggregation/src/indexer/pool-indexer.ts
@@ -83,6 +83,7 @@ export class PoolIndexer {
const hasDexConfig =
!!dexConfig &&
(dexConfig.uniswap_v2?.length ||
+ dexConfig.sushiswap?.length ||
dexConfig.uniswap_v3?.length ||
dexConfig.dodo?.length ||
dexConfig.custom?.length);
@@ -100,7 +101,14 @@ export class PoolIndexer {
// Index UniswapV2 pools
if (dexConfig.uniswap_v2) {
for (const config of dexConfig.uniswap_v2) {
- const pools = await this.indexUniswapV2Pools(config);
+ const pools = await this.indexUniswapV2Pools(config, 'uniswap_v2');
+ allPools.push(...pools);
+ }
+ }
+
+ if (dexConfig.sushiswap) {
+ for (const config of dexConfig.sushiswap) {
+ const pools = await this.indexUniswapV2Pools(config, 'sushiswap');
allPools.push(...pools);
}
}
@@ -208,7 +216,10 @@ export class PoolIndexer {
/**
* Index UniswapV2 pools from PairCreated events
*/
- private async indexUniswapV2Pools(config: UniswapV2Config): Promise {
+ private async indexUniswapV2Pools(
+ config: UniswapV2Config,
+ dexType: 'uniswap_v2' | 'sushiswap'
+ ): Promise {
const pools: LiquidityPool[] = [];
const factory = new ethers.Contract(config.factory, UNISWAP_V2_FACTORY_ABI, this.provider);
@@ -237,7 +248,7 @@ export class PoolIndexer {
poolAddress: pairAddress.toLowerCase(),
token0Address: token0.toLowerCase(),
token1Address: token1.toLowerCase(),
- dexType: 'uniswap_v2',
+ dexType,
factoryAddress: config.factory.toLowerCase(),
routerAddress: config.router?.toLowerCase(),
reserve0: reserve0.toString(),
@@ -255,7 +266,7 @@ export class PoolIndexer {
}
}
} catch (error) {
- logger.error(`Error indexing UniswapV2 pools:`, error);
+ logger.error(`Error indexing ${dexType} pools:`, error);
}
return pools;
@@ -390,7 +401,7 @@ export class PoolIndexer {
}
try {
- if (dexType === 'uniswap_v2') {
+ if (dexType === 'uniswap_v2' || dexType === 'sushiswap') {
const pair = new ethers.Contract(poolAddress, UNISWAP_V2_PAIR_ABI, this.provider);
const [reserve0, reserve1] = await pair.getReserves();
diff --git a/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts b/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts
index 8643d38..266548c 100644
--- a/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts
+++ b/services/token-aggregation/src/services/aggregator-route-matrix-generator.ts
@@ -23,6 +23,10 @@ function providerProtocol(provider: PlannerProvider): string {
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
+ case 'uniswap_v2':
+ return 'uniswap_v2';
+ case 'sushiswap':
+ return 'sushiswap';
case 'balancer':
return 'balancer';
case 'curve':
@@ -42,6 +46,10 @@ function providerLabel(provider: PlannerProvider): string {
return 'DODO V3 / D3MM';
case 'uniswap_v3':
return 'Uniswap V3';
+ case 'uniswap_v2':
+ return 'Uniswap V2';
+ case 'sushiswap':
+ return 'SushiSwap';
case 'balancer':
return 'Balancer';
case 'curve':
diff --git a/services/token-aggregation/src/services/best-execution-planner.test.ts b/services/token-aggregation/src/services/best-execution-planner.test.ts
index fee59ac..37a8fb5 100644
--- a/services/token-aggregation/src/services/best-execution-planner.test.ts
+++ b/services/token-aggregation/src/services/best-execution-planner.test.ts
@@ -183,7 +183,7 @@ describe('BestExecutionPlanner', () => {
expect(response.legs[0].provider).toBe('dodo_v3');
expect(response.estimatedAmountOut).toBe('211660490');
expect(response.routePlan).toBeDefined();
- expect(response.routePlan?.legs[0]?.provider).toBe(6);
+ expect(response.routePlan?.legs[0]?.provider).toBe(8);
expect(response.riskFlags).toContain('pilot-venue');
expect(response.riskFlags).not.toContain('manual-execution-only');
});
diff --git a/services/token-aggregation/src/services/best-execution-planner.ts b/services/token-aggregation/src/services/best-execution-planner.ts
index 4a6c424..d52a3d5 100644
--- a/services/token-aggregation/src/services/best-execution-planner.ts
+++ b/services/token-aggregation/src/services/best-execution-planner.ts
@@ -29,20 +29,24 @@ import {
const abiCoder = AbiCoder.defaultAbiCoder();
const ROUTER_V2_RECIPIENT_PLACEHOLDER = ZeroAddress;
const DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 = normalizeAddress('0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7');
-const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch', 'partner'];
+const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch', 'partner'];
const PROVIDER_ENUM: Partial> = {
dodo: 0,
uniswap_v3: 1,
- balancer: 2,
- curve: 3,
- one_inch: 4,
- partner: 5,
- dodo_v3: 6,
+ uniswap_v2: 2,
+ sushiswap: 3,
+ balancer: 4,
+ curve: 5,
+ one_inch: 6,
+ partner: 7,
+ dodo_v3: 8,
};
const PROVIDER_GAS_USD: Record = {
dodo: 0.22,
dodo_v3: 0.3,
uniswap_v3: 0.28,
+ uniswap_v2: 0.24,
+ sushiswap: 0.25,
balancer: 0.34,
curve: 0.29,
one_inch: 0.48,
diff --git a/services/token-aggregation/src/services/canonical-price-oracle.test.ts b/services/token-aggregation/src/services/canonical-price-oracle.test.ts
new file mode 100644
index 0000000..6ddfbca
--- /dev/null
+++ b/services/token-aggregation/src/services/canonical-price-oracle.test.ts
@@ -0,0 +1,99 @@
+import { getCanonicalTokenBySymbol } from '../config/canonical-tokens';
+import { getCanonicalPriceUsd, resolveCanonicalPriceUsd, resolveCanonicalPriceUsdForSpec } from './canonical-price-oracle';
+
+describe('canonical-price-oracle', () => {
+ it('pegs Chain 138 USD-family mirrors to one dollar', () => {
+ expect(getCanonicalPriceUsd(138, '0x71D6687F38b93CCad569Fa6352c876eea967201b')).toBe(1);
+ expect(getCanonicalPriceUsd(138, '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22')).toBe(1);
+ expect(getCanonicalPriceUsd(138, '0xf22258f57794CC8E06237084b353Ab30fFfa640b')).toBe(1);
+ });
+
+ it('anchors GRU v2 and lending wrappers to the matching c* ISO-4217 asset family', () => {
+ const cUsdcV2 = getCanonicalTokenBySymbol(138, 'cUSDC_V2');
+ expect(cUsdcV2?.addresses[138]).toBeTruthy();
+
+ expect(resolveCanonicalPriceUsd(138, String(cUsdcV2?.addresses[138]))).toMatchObject({
+ priceUsd: 1,
+ referenceSymbol: 'USD',
+ });
+
+ expect(
+ resolveCanonicalPriceUsdForSpec({
+ symbol: 'acUSDC',
+ name: 'Deposit cUSDC',
+ type: 'asset',
+ decimals: 6,
+ addresses: { 138: '0xac00000000000000000000000000000000000138' },
+ })
+ ).toMatchObject({
+ priceUsd: 1,
+ referenceSymbol: 'USD',
+ });
+
+ expect(
+ resolveCanonicalPriceUsdForSpec({
+ symbol: 'vdcEURC',
+ name: 'Debt cEURC (variable)',
+ type: 'debt',
+ decimals: 6,
+ addresses: { 138: '0xbdc0000000000000000000000000000000000138' },
+ })
+ ).toMatchObject({
+ priceUsd: 1.1780,
+ referenceSymbol: 'EUR',
+ });
+
+ expect(
+ resolveCanonicalPriceUsdForSpec({
+ symbol: 'sdcCADC',
+ name: 'Debt cCADC (stable)',
+ type: 'debt',
+ decimals: 6,
+ addresses: { 138: '0xcdc0000000000000000000000000000000000138' },
+ })
+ ).toMatchObject({
+ priceUsd: 0.7255928549430243,
+ referenceSymbol: 'CAD',
+ });
+ });
+
+ it('prices fiat GRU W tokens from the same ISO-4217 oracle references as the c* canonicals', () => {
+ const usdw = getCanonicalTokenBySymbol(25, 'USDW');
+ const eurw = getCanonicalTokenBySymbol(25, 'EURW');
+
+ expect(usdw?.addresses[25]).toBeTruthy();
+ expect(eurw?.addresses[25]).toBeTruthy();
+
+ expect(resolveCanonicalPriceUsd(25, String(usdw?.addresses[25]))).toMatchObject({
+ priceUsd: 1,
+ referenceSymbol: 'USD',
+ });
+ expect(resolveCanonicalPriceUsd(25, String(eurw?.addresses[25]))).toMatchObject({
+ priceUsd: 1.1780,
+ referenceSymbol: 'EUR',
+ });
+ });
+
+ it('resolves WETH9 and WETH10 to the ETH peg with 18-decimal canonical metadata', () => {
+ expect(resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')).toMatchObject({
+ priceUsd: 2490,
+ referenceSymbol: 'ETH',
+ });
+ expect(resolveCanonicalPriceUsd(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')).toMatchObject({
+ priceUsd: 2490,
+ referenceSymbol: 'ETH',
+ });
+ });
+
+ it('provides repo-local fallback pegs for commodity and monetary-unit canonicals', () => {
+ expect(resolveCanonicalPriceUsd(138, '0x290E52a8819A4fbD0714E517225429aA2B70EC6b')).toMatchObject({
+ referenceSymbol: 'XAU',
+ source: 'repo-fallback',
+ });
+ expect(resolveCanonicalPriceUsd(138, '0xcb7c000000000000000000000000000000000138')).toMatchObject({
+ priceUsd: 90000,
+ referenceSymbol: 'BTC',
+ source: 'repo-fallback',
+ });
+ });
+});
diff --git a/services/token-aggregation/src/services/canonical-price-oracle.ts b/services/token-aggregation/src/services/canonical-price-oracle.ts
new file mode 100644
index 0000000..242d0a4
--- /dev/null
+++ b/services/token-aggregation/src/services/canonical-price-oracle.ts
@@ -0,0 +1,193 @@
+import { type CanonicalTokenSpec, getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from '../config/canonical-tokens';
+
+export interface CanonicalPriceResolution {
+ priceUsd?: number;
+ referenceSymbol?: string;
+ source: 'env' | 'repo-fallback' | 'unresolved';
+}
+
+const FX_SNAPSHOT_GENERATED_AT = '2026-04-15';
+
+// Repo-local inferred FX snapshot from scripts/lib/extraction_gap_closure.py.
+const REPO_FALLBACK_PRICE_USD: Record = {
+ USD: 1,
+ EUR: 1.1780,
+ GBP: 1.3550353712543854,
+ AUD: 0.7136366390016357,
+ CAD: 0.7255928549430243,
+ CHF: 1.2776572668112798,
+ JPY: 0.006285683794888213,
+ XAU: 5163.3401260328355,
+ ETH: 2490,
+ BTC: 90000,
+ BNB: 610,
+ POL: 0.78,
+ AVAX: 48,
+ CELO: 0.72,
+ CRO: 0.14,
+ XDAI: 1,
+};
+
+function normalizeAddress(value: string): string {
+ return value.trim().toLowerCase();
+}
+
+function readEnvPrice(keys: string[]): number | undefined {
+ for (const key of keys) {
+ const raw = process.env[key];
+ if (!raw || raw.trim() === '') continue;
+ const parsed = Number(raw);
+ if (Number.isFinite(parsed) && parsed > 0) {
+ return parsed;
+ }
+ }
+ return undefined;
+}
+
+function resolveReferenceSymbol(spec: CanonicalTokenSpec): string | undefined {
+ const symbol = spec.symbol.toUpperCase();
+ const currencyCode = String(spec.currencyCode || '').trim().toUpperCase();
+
+ if (
+ symbol === 'WETH' ||
+ symbol === 'WETH9' ||
+ symbol === 'WETH10' ||
+ symbol === 'CETH' ||
+ symbol === 'CETHL2' ||
+ symbol === 'CWETH' ||
+ symbol === 'CWETHL2'
+ ) {
+ return 'ETH';
+ }
+
+ if (symbol === 'CBTC' || symbol === 'CWBTC') {
+ return 'BTC';
+ }
+
+ if (symbol === 'CXAUC' || symbol === 'CXAUT' || symbol === 'CAXAUC' || symbol === 'CAXAUT' || symbol === 'CWAXAUC' || symbol === 'CWAXAUT' || symbol === 'LIXAU') {
+ return 'XAU';
+ }
+
+ if (currencyCode) {
+ return currencyCode;
+ }
+
+ return undefined;
+}
+
+function resolvePegSourceSpec(spec: CanonicalTokenSpec, seenSymbols: Set = new Set()): CanonicalTokenSpec {
+ const symbol = spec.symbol.trim();
+ const symbolUpper = symbol.toUpperCase();
+ if (seenSymbols.has(symbolUpper)) {
+ return spec;
+ }
+
+ const nextSeen = new Set(seenSymbols);
+ nextSeen.add(symbolUpper);
+
+ const familySymbol = String(spec.familySymbol || '').trim();
+ if (familySymbol) {
+ const familyMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, familySymbol);
+ if (familyMatch) {
+ return resolvePegSourceSpec(familyMatch, nextSeen);
+ }
+ }
+
+ const lendingWrapperMatch = /^(ac|vdc|sdc)(.+)$/i.exec(symbol);
+ if (lendingWrapperMatch) {
+ const underlyingSymbol = `c${lendingWrapperMatch[2]}`;
+ const underlyingMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, underlyingSymbol);
+ if (underlyingMatch) {
+ return resolvePegSourceSpec(underlyingMatch, nextSeen);
+ }
+ }
+
+ return spec;
+}
+
+function getCanonicalTokenByAddressFromSymbolFamily(spec: CanonicalTokenSpec, symbol: string): CanonicalTokenSpec | undefined {
+ for (const [chainIdText, address] of Object.entries(spec.addresses)) {
+ if (!address || String(address).trim() === '') continue;
+ const chainId = Number(chainIdText);
+ if (!Number.isFinite(chainId)) continue;
+ const familyMatch = getCanonicalTokenBySymbol(chainId, symbol);
+ if (familyMatch) {
+ return familyMatch;
+ }
+ }
+ return undefined;
+}
+
+function resolveEnvPriceKeys(referenceSymbol: string): string[] {
+ const symbol = referenceSymbol.toUpperCase();
+ if (symbol === 'XAU') {
+ return [
+ 'CHAIN138_CANONICAL_PRICE_USD_XAU',
+ 'CANONICAL_PRICE_USD_XAU',
+ 'XAU_SPOT_USD',
+ 'GOLD_USD_PRICE',
+ ];
+ }
+
+ if (symbol === 'ETH') {
+ return [
+ 'CHAIN138_CANONICAL_PRICE_USD_ETH',
+ 'CANONICAL_PRICE_USD_ETH',
+ 'ETH_PRICE_USD',
+ 'CHAIN138_D3_PILOT_WETH_USD',
+ ];
+ }
+
+ return [
+ `CHAIN138_CANONICAL_PRICE_USD_${symbol}`,
+ `CANONICAL_PRICE_USD_${symbol}`,
+ `${symbol}_PRICE_USD`,
+ ];
+}
+
+export function resolveCanonicalPriceUsdForSpec(spec: CanonicalTokenSpec): CanonicalPriceResolution {
+ const pegSourceSpec = resolvePegSourceSpec(spec);
+ const referenceSymbol = resolveReferenceSymbol(pegSourceSpec);
+ if (!referenceSymbol) {
+ return { source: 'unresolved' };
+ }
+
+ const envPrice = readEnvPrice(resolveEnvPriceKeys(referenceSymbol));
+ if (envPrice !== undefined) {
+ return {
+ priceUsd: envPrice,
+ referenceSymbol,
+ source: 'env',
+ };
+ }
+
+ const fallback = REPO_FALLBACK_PRICE_USD[referenceSymbol];
+ if (fallback !== undefined) {
+ return {
+ priceUsd: fallback,
+ referenceSymbol,
+ source: 'repo-fallback',
+ };
+ }
+
+ return {
+ referenceSymbol,
+ source: 'unresolved',
+ };
+}
+
+export function resolveCanonicalPriceUsd(chainId: number, address: string): CanonicalPriceResolution {
+ const spec = getCanonicalTokenByAddress(chainId, normalizeAddress(address));
+ if (!spec) {
+ return { source: 'unresolved' };
+ }
+ return resolveCanonicalPriceUsdForSpec(spec);
+}
+
+export function getCanonicalPriceUsd(chainId: number, address: string): number | undefined {
+ return resolveCanonicalPriceUsd(chainId, address).priceUsd;
+}
+
+export function getCanonicalPriceSnapshotGeneratedAt(): string {
+ return FX_SNAPSHOT_GENERATED_AT;
+}
diff --git a/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts b/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts
index 8d72292..961a7bd 100644
--- a/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts
+++ b/services/token-aggregation/src/services/chain138-dodo-liquidity.test.ts
@@ -28,7 +28,20 @@ describe('estimateChain138DodoLiquidityUsd', () => {
expect(result.totalLiquidityUsd).toBe(210_830);
});
- it('keeps non-USD pairs at zero without a usable USD side', () => {
+ it('keeps WETH9 on the ETH peg even when live oracle price is unavailable', () => {
+ const result = estimateChain138DodoLiquidityUsd({
+ token0Address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
+ token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
+ reserve0: 10n * 10n ** 18n,
+ reserve1: 24_900n * 10n ** 6n,
+ });
+
+ expect(result.reserve0Usd).toBe(24_900);
+ expect(result.reserve1Usd).toBe(24_900);
+ expect(result.totalLiquidityUsd).toBe(49_800);
+ });
+
+ it('values non-USD canonical pairs from their repo-local peg references', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f',
token1Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b',
@@ -36,11 +49,22 @@ describe('estimateChain138DodoLiquidityUsd', () => {
reserve1: 5n * 10n ** 6n,
});
- expect(result).toEqual({
- reserve0Usd: 0,
- reserve1Usd: 0,
- totalLiquidityUsd: 0,
+ expect(result.reserve0Usd).toBe(24_900);
+ expect(result.reserve1Usd).toBeCloseTo(25_816.700630164178, 6);
+ expect(result.totalLiquidityUsd).toBeCloseTo(50_716.70063016418, 6);
+ });
+
+ it('values XAU/stable DODO pools from the canonical gold peg', () => {
+ const result = estimateChain138DodoLiquidityUsd({
+ token0Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b',
+ token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
+ reserve0: 5n * 10n ** 6n,
+ reserve1: 25_816n * 10n ** 6n,
});
+
+ expect(result.reserve0Usd).toBeCloseTo(25_816.700630164178, 6);
+ expect(result.reserve1Usd).toBe(25_816);
+ expect(result.totalLiquidityUsd).toBeCloseTo(51_632.70063016418, 6);
});
it('values cBTC/stable DODO pools using satoshi precision and the BTC fallback price', () => {
diff --git a/services/token-aggregation/src/services/chain138-dodo-liquidity.ts b/services/token-aggregation/src/services/chain138-dodo-liquidity.ts
index 407adb8..5b97858 100644
--- a/services/token-aggregation/src/services/chain138-dodo-liquidity.ts
+++ b/services/token-aggregation/src/services/chain138-dodo-liquidity.ts
@@ -1,9 +1,8 @@
import { formatUnits } from 'ethers';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
+import { getCanonicalPriceUsd } from './canonical-price-oracle';
const CHAIN_138 = 138;
-const DEFAULT_WETH_USD_PRICE = 2100;
-const DEFAULT_BTC_USD_PRICE = 90000;
export interface Chain138DodoLiquidityUsd {
reserve0Usd: number;
@@ -20,21 +19,6 @@ function decimalsForAddress(address: string): number {
return Number(spec?.decimals ?? 18);
}
-function isUsdAddress(address: string): boolean {
- const spec = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address));
- return spec?.currencyCode === 'USD';
-}
-
-function isWethLikeAddress(address: string): boolean {
- const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase();
- return symbol === 'WETH' || symbol === 'WETH10';
-}
-
-function isBtcLikeAddress(address: string): boolean {
- const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase();
- return symbol === 'CBTC';
-}
-
function parseAmount(value: bigint, decimals: number): number {
if (value <= 0n) return 0;
const parsed = Number(formatUnits(value, decimals));
@@ -60,54 +44,32 @@ export function estimateChain138DodoLiquidityUsd(args: {
const reserve1Amount = parseAmount(args.reserve1, decimalsForAddress(token1Address));
const price = parsePrice(args.price);
- const token0IsUsd = isUsdAddress(token0Address);
- const token1IsUsd = isUsdAddress(token1Address);
-
if (reserve0Amount <= 0 || reserve1Amount <= 0) {
return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
}
- if (token0IsUsd && token1IsUsd) {
- return {
- reserve0Usd: reserve0Amount,
- reserve1Usd: reserve1Amount,
- totalLiquidityUsd: reserve0Amount + reserve1Amount,
- };
+ let token0PriceUsd = getCanonicalPriceUsd(CHAIN_138, token0Address) ?? 0;
+ let token1PriceUsd = getCanonicalPriceUsd(CHAIN_138, token1Address) ?? 0;
+
+ if (price > 0) {
+ if (token1PriceUsd === 1 && token0PriceUsd !== 1) {
+ token0PriceUsd = price;
+ }
+ if (token0PriceUsd === 1 && token1PriceUsd !== 1) {
+ token1PriceUsd = price;
+ }
}
- if (token1IsUsd) {
- const reserve0Usd =
- price > 0
- ? reserve0Amount * price
- : isWethLikeAddress(token0Address)
- ? reserve0Amount * DEFAULT_WETH_USD_PRICE
- : isBtcLikeAddress(token0Address)
- ? reserve0Amount * DEFAULT_BTC_USD_PRICE
- : 0;
-
- return {
- reserve0Usd,
- reserve1Usd: reserve1Amount,
- totalLiquidityUsd: reserve0Usd > 0 ? reserve0Usd + reserve1Amount : 0,
- };
+ if (token0PriceUsd <= 0 || token1PriceUsd <= 0) {
+ return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
}
- if (token0IsUsd) {
- const reserve1Usd =
- price > 0
- ? reserve1Amount / price
- : isWethLikeAddress(token1Address)
- ? reserve1Amount * DEFAULT_WETH_USD_PRICE
- : isBtcLikeAddress(token1Address)
- ? reserve1Amount * DEFAULT_BTC_USD_PRICE
- : 0;
+ const reserve0Usd = reserve0Amount * token0PriceUsd;
+ const reserve1Usd = reserve1Amount * token1PriceUsd;
- return {
- reserve0Usd: reserve0Amount,
- reserve1Usd,
- totalLiquidityUsd: reserve1Usd > 0 ? reserve0Amount + reserve1Usd : 0,
- };
- }
-
- return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
+ return {
+ reserve0Usd,
+ reserve1Usd,
+ totalLiquidityUsd: reserve0Usd + reserve1Usd,
+ };
}
diff --git a/services/token-aggregation/src/services/planner-v2-types.ts b/services/token-aggregation/src/services/planner-v2-types.ts
index 693dddc..d39364d 100644
--- a/services/token-aggregation/src/services/planner-v2-types.ts
+++ b/services/token-aggregation/src/services/planner-v2-types.ts
@@ -1,4 +1,13 @@
-export type PlannerProvider = 'dodo' | 'dodo_v3' | 'uniswap_v3' | 'balancer' | 'curve' | 'one_inch' | 'partner';
+export type PlannerProvider =
+ | 'dodo'
+ | 'dodo_v3'
+ | 'uniswap_v3'
+ | 'uniswap_v2'
+ | 'sushiswap'
+ | 'balancer'
+ | 'curve'
+ | 'one_inch'
+ | 'partner';
export type PlannerLegKind = 'swap' | 'bridge';
export type PlannerDecision = 'direct-pool' | 'multi-hop' | 'swap-bridge-swap' | 'bridge-only' | 'unresolved';
export type ComplianceProfile = 'standard' | 'institutional';
diff --git a/services/token-aggregation/src/services/route-graph-builder.ts b/services/token-aggregation/src/services/route-graph-builder.ts
index e10894e..ec07d3b 100644
--- a/services/token-aggregation/src/services/route-graph-builder.ts
+++ b/services/token-aggregation/src/services/route-graph-builder.ts
@@ -30,6 +30,10 @@ function providerFromDexType(dexType: string): PlannerProvider | null {
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
+ case 'uniswap_v2':
+ return 'uniswap_v2';
+ case 'sushiswap':
+ return 'sushiswap';
default:
return null;
}
diff --git a/services/token-aggregation/src/services/valuation-precedence.test.ts b/services/token-aggregation/src/services/valuation-precedence.test.ts
new file mode 100644
index 0000000..f528e53
--- /dev/null
+++ b/services/token-aggregation/src/services/valuation-precedence.test.ts
@@ -0,0 +1,195 @@
+import {
+ isGruV2CwMeshEdgeToken,
+ pickExternalMarketDataForIndexer,
+ resolveUsdValuation,
+ mergeMarketWithValuation,
+} from './valuation-precedence';
+
+/** Bridged `cW*` on Ethereum; on Chain 138 the native form is `cUSDC` (`c*`), not `cW*`. */
+const ETH_MAINNET_CWUSDC = '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a';
+
+describe('valuation-precedence', () => {
+ const prevIndexerAge = process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS;
+ const prevEdgeCwDexFirst = process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST;
+
+ afterEach(() => {
+ if (prevIndexerAge === undefined) {
+ delete process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS;
+ } else {
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = prevIndexerAge;
+ }
+ if (prevEdgeCwDexFirst === undefined) {
+ delete process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST;
+ } else {
+ process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = prevEdgeCwDexFirst;
+ }
+ });
+
+ it('prefers fresh indexer price over canonical', () => {
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '3600';
+ const lu = new Date(Date.now() - 60_000);
+ const v = resolveUsdValuation({
+ chainId: 1,
+ normalizedAddress: '0x0000000000000000000000000000000000000001',
+ indexer: {
+ chainId: 1,
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ priceUsd: 2.5,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: lu,
+ },
+ coingecko: { priceUsd: 9.99, lastUpdated: new Date() },
+ });
+ expect(v.sourceLayer).toBe('indexer_market');
+ expect(v.priceUsd).toBe(2.5);
+ expect(v.stale).toBe(false);
+ });
+
+ it('skips stale indexer and uses external when indexer is too old', () => {
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60';
+ const lu = new Date(Date.now() - 120_000);
+ const v = resolveUsdValuation({
+ chainId: 1,
+ normalizedAddress: '0x0000000000000000000000000000000000000001',
+ indexer: {
+ chainId: 1,
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ priceUsd: 2.5,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: lu,
+ },
+ coingecko: { priceUsd: 9.99, lastUpdated: new Date() },
+ });
+ expect(v.sourceLayer).toBe('external_coingecko');
+ expect(v.priceUsd).toBe(9.99);
+ expect(v.stale).toBe(false);
+ });
+
+ it('falls back to stale indexer when nothing else is available', () => {
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60';
+ const lu = new Date(Date.now() - 120_000);
+ const v = resolveUsdValuation({
+ chainId: 1,
+ normalizedAddress: '0x0000000000000000000000000000000000000001',
+ indexer: {
+ chainId: 1,
+ tokenAddress: '0x0000000000000000000000000000000000000001',
+ priceUsd: 2.5,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: lu,
+ },
+ });
+ expect(v.sourceLayer).toBe('indexer_market');
+ expect(v.priceUsd).toBe(2.5);
+ expect(v.stale).toBe(true);
+ });
+
+ it('detects GRU v2 cW* on edge chains via canonical registry', () => {
+ expect(isGruV2CwMeshEdgeToken(1, ETH_MAINNET_CWUSDC)).toBe(true);
+ expect(isGruV2CwMeshEdgeToken(138, ETH_MAINNET_CWUSDC)).toBe(false);
+ expect(isGruV2CwMeshEdgeToken(1, '0x0000000000000000000000000000000000000001')).toBe(false);
+ });
+
+ it('for edge GRU v2 cW*, prefers DexScreener over CoinGecko when indexer is stale', () => {
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60';
+ const lu = new Date(Date.now() - 120_000);
+ const v = resolveUsdValuation({
+ chainId: 1,
+ normalizedAddress: ETH_MAINNET_CWUSDC,
+ indexer: {
+ chainId: 1,
+ tokenAddress: ETH_MAINNET_CWUSDC,
+ priceUsd: 2.5,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: lu,
+ },
+ coingecko: { priceUsd: 9.99, lastUpdated: new Date() },
+ dexscreener: { priceUsd: 1.0, lastUpdated: new Date() },
+ });
+ expect(v.gruV2CwEdgeDexPriority).toBe(true);
+ expect(v.sourceLayer).toBe('external_dexscreener');
+ expect(v.priceUsd).toBe(1.0);
+ expect(v.precedenceRank).toBe(2);
+ });
+
+ it('pickExternalMarketDataForIndexer prefers DexScreener for edge cW*', () => {
+ const dex = { priceUsd: 1, lastUpdated: new Date() };
+ const cg = { priceUsd: 2, lastUpdated: new Date() };
+ const picked = pickExternalMarketDataForIndexer(1, ETH_MAINNET_CWUSDC, {
+ coingecko: cg,
+ cmc: null,
+ dexscreener: dex,
+ });
+ expect(picked?.priceUsd).toBe(1);
+ });
+
+ it('pickExternalMarketDataForIndexer prefers CoinGecko when edge cW* flag is off', () => {
+ process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = '0';
+ const dex = { priceUsd: 1, lastUpdated: new Date() };
+ const cg = { priceUsd: 2, lastUpdated: new Date() };
+ const picked = pickExternalMarketDataForIndexer(1, ETH_MAINNET_CWUSDC, {
+ coingecko: cg,
+ cmc: null,
+ dexscreener: dex,
+ });
+ expect(picked?.priceUsd).toBe(2);
+ });
+
+ it('TOKEN_AGG_EDGE_CW_DEX_FIRST=0 restores CoinGecko-before-DexScreener for edge cW*', () => {
+ process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST = '0';
+ process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '60';
+ const lu = new Date(Date.now() - 120_000);
+ const v = resolveUsdValuation({
+ chainId: 1,
+ normalizedAddress: ETH_MAINNET_CWUSDC,
+ indexer: {
+ chainId: 1,
+ tokenAddress: ETH_MAINNET_CWUSDC,
+ priceUsd: 2.5,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: lu,
+ },
+ coingecko: { priceUsd: 9.99, lastUpdated: new Date() },
+ dexscreener: { priceUsd: 1.0, lastUpdated: new Date() },
+ });
+ expect(v.gruV2CwEdgeDexPriority).toBe(false);
+ expect(v.sourceLayer).toBe('external_coingecko');
+ expect(v.priceUsd).toBe(9.99);
+ });
+
+ it('mergeMarketWithValuation applies priced layer onto indexer row', () => {
+ const pricing = resolveUsdValuation({
+ chainId: 138,
+ normalizedAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase(),
+ indexer: null,
+ coingecko: undefined,
+ });
+ const m = mergeMarketWithValuation(138, '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', null, pricing);
+ expect(m?.priceUsd).toBe(pricing.priceUsd);
+ });
+});
diff --git a/services/token-aggregation/src/services/valuation-precedence.ts b/services/token-aggregation/src/services/valuation-precedence.ts
new file mode 100644
index 0000000..99b7842
--- /dev/null
+++ b/services/token-aggregation/src/services/valuation-precedence.ts
@@ -0,0 +1,276 @@
+import type { MarketData } from '../adapters/base-adapter';
+import type { TokenMarketData } from '../database/repositories/token-repo';
+import { getChainConfig } from '../config/chains';
+import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
+import { resolveCanonicalPriceUsd } from './canonical-price-oracle';
+
+const CHAIN_138 = 138;
+
+/**
+ * When true (default), bridged GRU v2 **`cW*`** tokens (non–Chain-138) prefer DexScreener before CoinGecko/CMC.
+ * Native Chain 138 assets use **`c*`** naming (e.g. cUSDT); those are not bridged `cW*` and this flag does not apply on 138.
+ */
+function readEdgeCwDexFirst(): boolean {
+ const raw = process.env.TOKEN_AGG_EDGE_CW_DEX_FIRST;
+ if (raw === undefined || raw === '') return true;
+ return raw === '1' || raw.toLowerCase() === 'true' || raw.toLowerCase() === 'yes';
+}
+
+/**
+ * True for **bridged** GRU v2 transport on a public network: symbols **`cW*`** (e.g. cWUSDT, cWEURC, cWXAUC, cWETH).
+ * On **Chain 138**, native GRU v2 uses **`c*`** (cUSDT, cUSDC, …), not `cW*` — this helper always returns false on 138
+ * so valuation for home-chain assets follows the default layer order.
+ */
+export function isGruV2CwMeshEdgeToken(chainId: number, normalizedAddress: string): boolean {
+ if (chainId === CHAIN_138) return false;
+ const spec = getCanonicalTokenByAddress(chainId, normalizedAddress.toLowerCase());
+ if (!spec) return false;
+ return /^cW/i.test(spec.symbol.trim());
+}
+
+export type PriceSourceLayer =
+ | 'indexer_market'
+ | 'external_coingecko'
+ | 'external_coinmarketcap'
+ | 'external_dexscreener'
+ | 'canonical_env'
+ | 'canonical_repo_fallback'
+ | 'none';
+
+export interface ExplorerLinks {
+ chainId: number;
+ explorerBaseUrl?: string;
+ /** Primary explorer URL for this contract (Blockscout/Etherscan style). */
+ addressUrl?: string;
+ /** Alias for UIs that expect a “token page”; often same as address on explorers. */
+ tokenUrl?: string;
+}
+
+export interface TokenPricing {
+ /** Omitted when no layer produced a price. */
+ priceUsd?: number;
+ sourceLayer: PriceSourceLayer;
+ /** 1 = highest precedence (indexer). */
+ precedenceRank: number;
+ /** True when the chosen layer is older than configured max age (indexer only today). */
+ stale: boolean;
+ maxAgeSeconds: number;
+ /** ISO 8601 — effective “as of” for the displayed price. */
+ asOf: string;
+ ageSeconds?: number;
+ indexerLastUpdated?: string;
+ referenceSymbol?: string;
+ canonicalResolutionSource?: 'env' | 'repo-fallback' | 'unresolved';
+ /** Bridged `cW*` on an edge chain used DexScreener-priority ordering (native `c*` on 138 never sets this). */
+ gruV2CwEdgeDexPriority?: boolean;
+}
+
+export interface ValuationInput {
+ chainId: number;
+ normalizedAddress: string;
+ indexer: TokenMarketData | null;
+ coingecko?: MarketData | null;
+ cmc?: MarketData | null;
+ dexscreener?: MarketData | null;
+}
+
+function readIndexerMaxAgeSeconds(): number {
+ const raw = process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS;
+ const n = raw ? parseInt(raw, 10) : NaN;
+ return Number.isFinite(n) && n > 0 ? n : 900;
+}
+
+function ageMsOf(date: Date): number {
+ return Date.now() - date.getTime();
+}
+
+function iso(d: Date): string {
+ return d.toISOString();
+}
+
+/**
+ * Layered USD valuation:
+ * - Default: indexer (staleness-aware) → CoinGecko → CMC → DexScreener → canonical env/repo.
+ * - **Bridged GRU v2 `cW*` on edge networks** (not Chain 138; native **`c*`** lives on 138 only): indexer →
+ * **DexScreener → CoinGecko → CMC** → canonical, so that chain’s DEX/aggregated prices lead before generic repo FX.
+ * If the indexer price exists but is stale, we fall through to fresher layers before accepting the stale indexer.
+ */
+export function resolveUsdValuation(input: ValuationInput): TokenPricing {
+ const maxAgeSeconds = readIndexerMaxAgeSeconds();
+ const { chainId, normalizedAddress, indexer } = input;
+
+ const edgeCwDexPriority = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress);
+
+ const canonical = resolveCanonicalPriceUsd(chainId, normalizedAddress);
+ const referenceSymbol = canonical.referenceSymbol;
+ const canonicalResolutionSource = canonical.source === 'unresolved' ? 'unresolved' : canonical.source;
+
+ type Candidate = {
+ layer: PriceSourceLayer;
+ rank: number;
+ priceUsd: number;
+ stale: boolean;
+ asOf: Date;
+ };
+
+ const out: Candidate[] = [];
+
+ if (indexer?.priceUsd !== undefined && indexer.priceUsd !== null) {
+ const lu = indexer.lastUpdated instanceof Date ? indexer.lastUpdated : new Date(indexer.lastUpdated);
+ const stale = ageMsOf(lu) > maxAgeSeconds * 1000;
+ out.push({
+ layer: 'indexer_market',
+ rank: 1,
+ priceUsd: indexer.priceUsd,
+ stale,
+ asOf: lu,
+ });
+ }
+
+ const pushExternal = (layer: PriceSourceLayer, rank: number, md: MarketData | null | undefined): void => {
+ if (!md || md.priceUsd === undefined || md.priceUsd === null) return;
+ const asOf = md.lastUpdated instanceof Date ? md.lastUpdated : new Date();
+ out.push({
+ layer,
+ rank,
+ priceUsd: md.priceUsd,
+ stale: false,
+ asOf,
+ });
+ };
+
+ if (edgeCwDexPriority) {
+ pushExternal('external_dexscreener', 2, input.dexscreener);
+ pushExternal('external_coingecko', 3, input.coingecko);
+ pushExternal('external_coinmarketcap', 4, input.cmc);
+ } else {
+ pushExternal('external_coingecko', 2, input.coingecko);
+ pushExternal('external_coinmarketcap', 3, input.cmc);
+ pushExternal('external_dexscreener', 4, input.dexscreener);
+ }
+
+ if (canonical.priceUsd !== undefined && canonical.source === 'env') {
+ out.push({
+ layer: 'canonical_env',
+ rank: 5,
+ priceUsd: canonical.priceUsd,
+ stale: false,
+ asOf: new Date(),
+ });
+ }
+
+ if (canonical.priceUsd !== undefined && canonical.source === 'repo-fallback') {
+ out.push({
+ layer: 'canonical_repo_fallback',
+ rank: 6,
+ priceUsd: canonical.priceUsd,
+ stale: false,
+ asOf: new Date(),
+ });
+ }
+
+ const byRank = [...out].sort((a, b) => a.rank - b.rank);
+ const fresh = byRank.find((c) => !c.stale);
+ const winner = fresh || byRank[0];
+
+ if (!winner) {
+ return {
+ sourceLayer: 'none',
+ precedenceRank: 99,
+ stale: true,
+ maxAgeSeconds,
+ asOf: iso(new Date()),
+ referenceSymbol,
+ canonicalResolutionSource,
+ gruV2CwEdgeDexPriority: edgeCwDexPriority,
+ };
+ }
+
+ const ageSeconds = Math.max(0, Math.floor(ageMsOf(winner.asOf) / 1000));
+
+ return {
+ priceUsd: winner.priceUsd,
+ sourceLayer: winner.layer,
+ precedenceRank: winner.rank,
+ stale: winner.stale,
+ maxAgeSeconds,
+ asOf: iso(winner.asOf),
+ ageSeconds,
+ indexerLastUpdated: indexer?.lastUpdated
+ ? indexer.lastUpdated instanceof Date
+ ? iso(indexer.lastUpdated)
+ : String(indexer.lastUpdated)
+ : undefined,
+ referenceSymbol,
+ canonicalResolutionSource,
+ gruV2CwEdgeDexPriority: edgeCwDexPriority,
+ };
+}
+
+/**
+ * Chooses one external feed for persisting indexer `market_data` rows — same **coingecko / dexscreener / cmc** order as
+ * {@link resolveUsdValuation} (bridged **`cW*`** on edge chains prefer DexScreener when enabled).
+ */
+export function pickExternalMarketDataForIndexer(
+ chainId: number,
+ normalizedAddress: string,
+ sources: { coingecko: MarketData | null; cmc: MarketData | null; dexscreener: MarketData | null }
+): MarketData | null {
+ const edgeCw = readEdgeCwDexFirst() && isGruV2CwMeshEdgeToken(chainId, normalizedAddress);
+ if (edgeCw) {
+ return sources.dexscreener ?? sources.coingecko ?? sources.cmc ?? null;
+ }
+ return sources.coingecko ?? sources.dexscreener ?? sources.cmc ?? null;
+}
+
+/**
+ * Merge DB/indexer market row with layered valuation (priceUsd + optional fields).
+ */
+export function mergeMarketWithValuation(
+ chainId: number,
+ normalizedAddress: string,
+ marketData: TokenMarketData | null,
+ pricing: TokenPricing
+): TokenMarketData | null {
+ const addr = normalizedAddress.toLowerCase();
+ if (pricing.sourceLayer === 'none' && !marketData) {
+ return null;
+ }
+ if (!marketData) {
+ if (pricing.priceUsd === undefined) {
+ return null;
+ }
+ return {
+ chainId,
+ tokenAddress: addr,
+ priceUsd: pricing.priceUsd,
+ volume24h: 0,
+ volume7d: 0,
+ volume30d: 0,
+ liquidityUsd: 0,
+ holdersCount: 0,
+ transfers24h: 0,
+ lastUpdated: new Date(pricing.asOf),
+ };
+ }
+ return {
+ ...marketData,
+ priceUsd: pricing.priceUsd !== undefined ? pricing.priceUsd : marketData.priceUsd,
+ };
+}
+
+export function buildExplorerLinks(chainId: number, address: string): ExplorerLinks {
+ const cfg = getChainConfig(chainId);
+ const base = cfg?.explorerUrl?.replace(/\/$/, '') || '';
+ const lower = address.toLowerCase();
+ if (!base) {
+ return { chainId };
+ }
+ const url = `${base}/address/${lower}`;
+ return {
+ chainId,
+ explorerBaseUrl: base,
+ addressUrl: url,
+ tokenUrl: url,
+ };
+}
diff --git a/services/tron-relay/src/TronRelayService.ts b/services/tron-relay/src/TronRelayService.ts
new file mode 100644
index 0000000..9a6fadc
--- /dev/null
+++ b/services/tron-relay/src/TronRelayService.ts
@@ -0,0 +1,22 @@
+import {
+ NON_EVM_RELAY_LIFECYCLE,
+ type NonEvmNetworkPolicy,
+ type NonEvmRelayObservation
+} from '../../non-evm-relay/lifecycle';
+
+export const TRON_RELAY_POLICY: NonEvmNetworkPolicy = {
+ identifier: 'Tron',
+ relayMode: 'custom_relay_scaffold',
+ destinationProgramModel: 'trc20_or_bridge_wrapped_cw',
+ signerFundingPolicy: 'trx_operator_signer',
+ finalityPolicy: 'block_finality>=20',
+ publicExposureStatus: 'operator_ready'
+};
+
+export class TronRelayService {
+ readonly lifecycle = NON_EVM_RELAY_LIFECYCLE;
+
+ recordObservation(observation: NonEvmRelayObservation): NonEvmRelayObservation {
+ return observation;
+ }
+}
diff --git a/test/wrapped-lp-public/WrappedLPProgram.t.sol b/test/wrapped-lp-public/WrappedLPProgram.t.sol
new file mode 100644
index 0000000..2293209
--- /dev/null
+++ b/test/wrapped-lp-public/WrappedLPProgram.t.sol
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+import {Chain138LPLocker} from "../../contracts/wrapped-lp-public/Chain138LPLocker.sol";
+import {WLPReceiptToken} from "../../contracts/wrapped-lp-public/WLPReceiptToken.sol";
+import {PublicChainMintController} from "../../contracts/wrapped-lp-public/PublicChainMintController.sol";
+import {WLPRedemptionGateway} from "../../contracts/wrapped-lp-public/WLPRedemptionGateway.sol";
+import {WrappedLPNAVVault} from "../../contracts/wrapped-lp-public/WrappedLPNAVVault.sol";
+import {WLPNAVOracle} from "../../contracts/wrapped-lp-public/WLPNAVOracle.sol";
+
+contract MockERC20 is ERC20 {
+ constructor(string memory n, string memory s) ERC20(n, s) {}
+
+ function mint(address to, uint256 a) external {
+ _mint(to, a);
+ }
+}
+
+contract WrappedLPProgramTest is Test {
+ MockERC20 lp;
+ Chain138LPLocker locker;
+ WLPReceiptToken wlp;
+ PublicChainMintController mintCtl;
+ WLPRedemptionGateway gateway;
+
+ address admin = address(this);
+ address relayer = address(0xBEEF);
+ address user = address(0xA11CE);
+
+ function setUp() public {
+ lp = new MockERC20("DODO LP", "DLP");
+ locker = new Chain138LPLocker(address(lp), admin);
+ wlp = new WLPReceiptToken("Wrapped LP", "wLP", 18, admin);
+ mintCtl = new PublicChainMintController(address(wlp), address(locker), admin);
+ gateway = new WLPRedemptionGateway(address(wlp), admin);
+
+ wlp.grantRole(wlp.MINTER_ROLE(), address(mintCtl));
+ wlp.grantRole(wlp.BURNER_ROLE(), address(gateway));
+ mintCtl.grantRole(mintCtl.RELAYER_ROLE(), relayer);
+ locker.grantRole(locker.BRIDGE_RELEASE_ROLE(), relayer);
+ }
+
+ function test_lock_mint_release_invariant() public {
+ uint256 amt = 1000e18;
+ lp.mint(user, amt);
+ vm.startPrank(user);
+ lp.approve(address(locker), amt);
+ bytes32 lockRef = locker.deposit(amt);
+ vm.stopPrank();
+
+ assertEq(locker.totalEscrowed(), amt);
+
+ vm.prank(relayer);
+ mintCtl.mintForLock(lockRef, user, amt);
+ assertEq(wlp.balanceOf(user), amt);
+
+ vm.expectRevert();
+ vm.prank(relayer);
+ mintCtl.mintForLock(lockRef, user, amt);
+
+ vm.prank(user);
+ gateway.requestRedeem(amt);
+ assertEq(wlp.balanceOf(user), 0);
+
+ vm.prank(relayer);
+ locker.releaseAmount(user, amt);
+ assertEq(locker.totalEscrowed(), 0);
+ assertEq(lp.balanceOf(user), amt);
+ }
+
+ function test_nav_vault_cap() public {
+ MockERC20 usdc = new MockERC20("USDC", "USDC");
+ WrappedLPNAVVault vault = new WrappedLPNAVVault(
+ usdc,
+ "Wrapped LP NAV Vault",
+ "wLPV",
+ admin
+ );
+ vault.setDepositCap(1_000_000e18);
+ usdc.mint(user, 2_000_000e18);
+ vm.startPrank(user);
+ usdc.approve(address(vault), type(uint256).max);
+ vm.expectRevert();
+ vault.deposit(1_500_000e18, user);
+ vm.stopPrank();
+ }
+
+ function test_nav_oracle_stale() public {
+ WLPNAVOracle oracle = new WLPNAVOracle(admin, 3600);
+ oracle.grantRole(oracle.KEEPER_ROLE(), relayer);
+ vm.prank(relayer);
+ oracle.submitAnswer(1e8);
+ assertFalse(oracle.isStale());
+ vm.warp(block.timestamp + 4000);
+ assertTrue(oracle.isStale());
+ }
+
+ /// @notice Fuzz: distinct deposits yield distinct lockRefs and independent mints.
+ function testFuzz_lockRef_unique(uint128 amtA, uint128 amtB) public {
+ vm.assume(amtA > 0 && amtB > 0);
+ vm.assume(amtA != amtB);
+
+ uint256 a = uint256(amtA) * 1e10;
+ uint256 b = uint256(amtB) * 1e10;
+
+ lp.mint(user, a + b);
+ vm.startPrank(user);
+ lp.approve(address(locker), a + b);
+ bytes32 rA = locker.deposit(a);
+ bytes32 rB = locker.deposit(b);
+ vm.stopPrank();
+
+ assertTrue(rA != rB);
+
+ vm.startPrank(relayer);
+ mintCtl.mintForLock(rA, user, a);
+ mintCtl.mintForLock(rB, user, b);
+ vm.stopPrank();
+
+ assertEq(wlp.balanceOf(user), a + b);
+ }
+}