Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/evm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: Run Forge build
run: |
Expand Down Expand Up @@ -103,7 +103,7 @@ jobs:
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly
version: stable

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand Down
228 changes: 43 additions & 185 deletions evm/script/DeployIntentGateway.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ pragma solidity ^0.8.17;
import "forge-std/Script.sol";
import "stringutils/strings.sol";

import {IntentGatewayV2, Params, Deployment} from "../src/apps/IntentGatewayV2.sol";
import {IntentGatewayV2, Params} from "../src/apps/IntentGatewayV2.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {BaseScript} from "./BaseScript.sol";
import {CallDispatcher} from "../src/utils/CallDispatcher.sol";
import {SolverAccount} from "../src/apps/intentsv2/SolverAccount.sol";
Expand All @@ -17,200 +18,57 @@ contract DeployScript is BaseScript {
/// @notice Main deployment logic - called by BaseScript's run() functions
/// @dev This function is called within a broadcast context
function deploy() internal override {
IntentGatewayV2 intentGateway = new IntentGatewayV2{salt: salt}(admin);
console.log("IntentGateway deployed at:", address(intentGateway));

SolverAccount solverAccount = new SolverAccount{salt: salt}(address(intentGateway));
console.log("SolverAccount deployed at:", address(solverAccount));

// Deploy implementation and proxy via CREATE2 with the same salt. The proxy is initialized
// atomically through its init data. The cross-chain peer registry is passed in by chain id
// only — `initialize` binds each to `address(this)` — so no peer address is embedded in the
// init data. The address depends on (impl address, salt, params, peer chain ids), all of
// which are identical across chains, keeping the proxy address identical everywhere.
address priceOracle = address(0);
// address priceOracle = deployPriceOracle(address(intentGateway));

Deployment[] memory deployments;
IntentGatewayV2 implementation = new IntentGatewayV2{salt: salt}(admin);
bytes[] memory peerChains;
if (config.get("is_mainnet").toBool()) {
deployments = new Deployment[](9);
deployments[0] = Deployment({
chain: StateMachine.evm(1), // ethereum
gateway: address(intentGateway)
});
deployments[1] = Deployment({
chain: StateMachine.evm(10), // optimism
gateway: address(intentGateway)
});
deployments[2] = Deployment({
chain: StateMachine.evm(42161), // arbitrum
gateway: address(intentGateway)
});
deployments[3] = Deployment({
chain: StateMachine.evm(8453), // base
gateway: address(intentGateway)
});
deployments[4] = Deployment({
chain: StateMachine.evm(56), // bsc
gateway: address(intentGateway)
});
deployments[5] = Deployment({
chain: StateMachine.evm(100), // gnosis
gateway: address(intentGateway)
});
deployments[6] = Deployment({
chain: StateMachine.evm(137), // polygon
gateway: address(intentGateway)
});
deployments[7] = Deployment({
chain: StateMachine.evm(420420419), // polkadot
gateway: address(intentGateway)
});
deployments[8] = Deployment({
chain: StateMachine.evm(1868), // soneium
gateway: address(intentGateway)
});
peerChains = new bytes[](9);
peerChains[0] = StateMachine.evm(1); // ethereum
peerChains[1] = StateMachine.evm(10); // optimism
peerChains[2] = StateMachine.evm(42161); // arbitrum
peerChains[3] = StateMachine.evm(8453); // base
peerChains[4] = StateMachine.evm(56); // bsc
peerChains[5] = StateMachine.evm(100); // gnosis
peerChains[6] = StateMachine.evm(137); // polygon
peerChains[7] = StateMachine.evm(420420419); // polkadot
peerChains[8] = StateMachine.evm(1868); // soneium
} else {
deployments = new Deployment[](2);
deployments[0] = Deployment({
chain: StateMachine.evm(97), // bsc testnet (chapel)
gateway: address(intentGateway)
});
deployments[1] = Deployment({
chain: StateMachine.evm(80002), // polygon amoy
gateway: address(intentGateway)
});
peerChains = new bytes[](2);
peerChains[0] = StateMachine.evm(97); // bsc testnet (chapel)
peerChains[1] = StateMachine.evm(80002); // polygon amoy
}

intentGateway.init(
Params({
host: HOST_ADDRESS,
dispatcher: config.get("CALL_DISPATCHER").toAddress(),
solverSelection: config.get("7702").toBool(),
surplusShareBps: 5_000, // 50%
protocolFeeBps: 30, // 0.3%
priceOracle: address(priceOracle)
}),
deployments
bytes memory initData = abi.encodeCall(
IntentGatewayV2.initialize,
Comment thread
seunlanlege marked this conversation as resolved.
(
Params({
host: HOST_ADDRESS,
dispatcher: config.get("CALL_DISPATCHER").toAddress(),
solverSelection: config.get("7702").toBool(),
surplusShareBps: 5_000, // 50%
protocolFeeBps: 30, // 0.3%
priceOracle: priceOracle
}),
peerChains
)
);
ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}(address(implementation), initData);
IntentGatewayV2 intentGateway = IntentGatewayV2(payable(address(proxy)));
SolverAccount solverAccount = new SolverAccount{salt: salt}(address(intentGateway));

vm.stopBroadcast();

console.log("IntentGateway implementation deployed at:", address(implementation));
console.log("IntentGateway proxy deployed at:", address(intentGateway));
console.log("SolverAccount deployed at:", address(solverAccount));

config.set("INTENT_GATEWAY_V2", address(intentGateway));
config.set("SOLVER_ACCOUNT", address(solverAccount));
config.set("PRICE_ORACLE", address(priceOracle));
}

function deployPriceOracle(address intentGateway) internal returns (address) {
VWAPOracle priceOracle = new VWAPOracle{salt: salt}(admin, intentGateway);
console.log("VWAPOracle deployed at:", address(priceOracle));

// Initialize price oracle with token decimals
VWAPOracle.TokenDecimalsUpdate[] memory decimalsUpdates = new VWAPOracle.TokenDecimalsUpdate[](9);

// Ethereum
decimalsUpdates[0].sourceChain = bytes("EVM-1");
decimalsUpdates[0].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[0].tokens[0] = VWAPOracle.TokenDecimal({
token: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC
decimals: 6
});
decimalsUpdates[0].tokens[1] = VWAPOracle.TokenDecimal({
token: 0xdAC17F958D2ee523a2206206994597C13D831ec7, // USDT
decimals: 6
});

// Arbitrum
decimalsUpdates[1].sourceChain = bytes("EVM-42161");
decimalsUpdates[1].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[1].tokens[0] = VWAPOracle.TokenDecimal({
token: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, // USDC
decimals: 6
});
decimalsUpdates[1].tokens[1] = VWAPOracle.TokenDecimal({
token: 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9, // USDT
decimals: 6
});

// Optimism
decimalsUpdates[2].sourceChain = bytes("EVM-10");
decimalsUpdates[2].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[2].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85, // USDC
decimals: 6
});
decimalsUpdates[2].tokens[1] = VWAPOracle.TokenDecimal({
token: 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58, // USDT
decimals: 6
});

// Base
decimalsUpdates[3].sourceChain = bytes("EVM-8453");
decimalsUpdates[3].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[3].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913, // USDC
decimals: 6
});
decimalsUpdates[3].tokens[1] = VWAPOracle.TokenDecimal({
token: 0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2, // USDT
decimals: 6
});

// BSC
decimalsUpdates[4].sourceChain = bytes("EVM-56");
decimalsUpdates[4].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[4].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d, // USDC
decimals: 18
});
decimalsUpdates[4].tokens[1] = VWAPOracle.TokenDecimal({
token: 0x55d398326f99059fF775485246999027B3197955, // USDT
decimals: 18
});

// Gnosis
decimalsUpdates[5].sourceChain = bytes("EVM-100");
decimalsUpdates[5].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[5].tokens[0] = VWAPOracle.TokenDecimal({
token: 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83, // USDC (WXDAI)
decimals: 6
});
decimalsUpdates[5].tokens[1] = VWAPOracle.TokenDecimal({
token: 0x4ECaBa5870353805a9F068101A40E0f32ed605C6, // USDT
decimals: 6
});

// Polygon
decimalsUpdates[6].sourceChain = bytes("EVM-137");
decimalsUpdates[6].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[6].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359, // USDC
decimals: 6
});
decimalsUpdates[6].tokens[1] = VWAPOracle.TokenDecimal({
token: 0xc2132D05D31c914a87C6611C10748AEb04B58e8F, // USDT
decimals: 6
});

// Unichain
decimalsUpdates[7].sourceChain = bytes("EVM-130");
decimalsUpdates[7].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[7].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x078D782b760474a361dDA0AF3839290b0EF57AD6, // USDC
decimals: 6
});
decimalsUpdates[7].tokens[1] = VWAPOracle.TokenDecimal({
token: 0x9151434b16b9763660705744891fA906F660EcC5, // USDT
decimals: 6
});

// Tron
decimalsUpdates[8].sourceChain = bytes("EVM-728126428");
decimalsUpdates[8].tokens = new VWAPOracle.TokenDecimal[](2);
decimalsUpdates[8].tokens[0] = VWAPOracle.TokenDecimal({
token: 0x742b023b58488B4be587bBA63ed11134b02217B3, // USDC
decimals: 6
});
decimalsUpdates[8].tokens[1] = VWAPOracle.TokenDecimal({
token: 0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C, // USDT
decimals: 6
});
priceOracle.init(HOST_ADDRESS, decimalsUpdates);

return address(priceOracle);
}
}
55 changes: 35 additions & 20 deletions evm/src/apps/IntentGatewayV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {IDispatcher} from "@hyperbridge/core/interfaces/IDispatcher.sol";
import {IIntentPriceOracle} from "@hyperbridge/core/apps/IntentPriceOracle.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import {ReentrancyGuardTransient} from "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IUniswapV2Router02} from "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
Expand Down Expand Up @@ -57,16 +58,24 @@ import {
* \ /
* IntentGatewayV2
*/
contract IntentGatewayV2 is IntrinsicIntents, ExtrinsicIntents, ReentrancyGuardTransient {
contract IntentGatewayV2 is IntrinsicIntents, ExtrinsicIntents, ReentrancyGuardTransient, Initializable {
using SafeERC20 for IERC20;

/**
* @dev Initializes the EIP-712 domain with name "IntentGateway" and version "2".
* Sets the initial admin who has one-time authority to call `setParams`.
* @param admin The address that will have permission to set initial parameters.
* @dev Owner authorized to call `initialize` once on the proxy. Immutable (zero storage
* slots), so it must be byte-identical across chains or the deterministic proxy address diverges.
*/
constructor(address admin) EIP712("IntentGateway", "2") {
_admin = admin;
address private immutable _owner;

/**
* @dev Initializes the EIP-712 domain with name "IntentGateway" and version "2",
* records the owner authorized to initialize the proxy, and locks this raw
* implementation so it can never be initialized directly.
* @param owner The address permitted to call `initialize` on the proxy.
*/
constructor(address owner) EIP712("IntentGateway", "2") {
_owner = owner;
_disableInitializers();
}

/**
Expand All @@ -87,25 +96,31 @@ contract IntentGatewayV2 is IntrinsicIntents, ExtrinsicIntents, ReentrancyGuardT
}

/**
* @dev One-time parameter initialization. Can only be called by the admin set in
* the constructor. After successful execution, the admin is burned (set to address(0)),
* preventing any further calls.
*
* Subsequent parameter updates must come through Hyperbridge governance via the `onAccept` callback.
* @dev One-time initialization run against the proxy's storage. The `initializer` modifier
* caps it to a single call and the owner gate restricts who may call it. Registers the initial
* cross-chain peers (each bound to `address(this)`); a chain never registered here or later via
* `onAccept` is rejected by `_instance` with `UnknownInstance`. Later config changes go through
* `onAccept` governance.
*
* @param p The initial gateway configuration parameters.
* @param deployments The initial gateway cross-chain peers
* @param peerChains The state-machine ids of the cross-chain peers to register. Each is bound to
* this gateway's own address (`address(this)`), which is identical across chains under
* deterministic CREATE2 deployment — so no peer address is carried in (or depended on by) the
* proxy's init data.
*/
function init(Params memory p, Deployment[] memory deployments) public {
if (msg.sender != _admin) revert Unauthorized();

uint256 deploymentsLength = deployments.length;
for (uint256 i = 0; i < deploymentsLength; i++) {
_addDeployment(deployments[i]);
function initialize(Params memory p, bytes[] memory peerChains) public initializer {
if (msg.sender != _owner) revert Unauthorized();

uint256 peersLength = peerChains.length;
for (uint256 i = 0; i < peersLength; i++) {
Deployment memory deployment = Deployment({
chain: peerChains[i],
gateway: address(this)
});
_addDeployment(deployment);
}
_validateParams(p);
_params = p;
_admin = address(0);
}

/**
Expand All @@ -118,7 +133,7 @@ contract IntentGatewayV2 is IntrinsicIntents, ExtrinsicIntents, ReentrancyGuardT

/**
* @dev Returns the registered gateway address for a given state machine.
* Falls back to this contract's address if no remote deployment is registered.
* Reverts with `UnknownInstance` if no remote deployment is registered.
* @param stateMachineId The raw state machine identifier bytes.
* @return The gateway address for the given state machine.
*/
Expand Down
6 changes: 6 additions & 0 deletions evm/src/apps/intentsv2/ExtrinsicIntents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
} from "@hyperbridge/core/apps/IntentGatewayV2.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";


/**
Expand Down Expand Up @@ -280,6 +281,8 @@ abstract contract ExtrinsicIntents is IntentsBase, HyperApp {
* protocol fees. Only Hyperbridge may dispatch this request.
* - SweepDust: Transfers accumulated protocol dust to a specified beneficiary.
* Only Hyperbridge may dispatch this request.
* - UpgradeContract: Points the ERC-1967 proxy at a new implementation, optionally
* running migration calldata atomically. Only Hyperbridge may dispatch this request.
*
* @param incoming The incoming post request from Hyperbridge.
*/
Expand All @@ -299,6 +302,9 @@ abstract contract ExtrinsicIntents is IntentsBase, HyperApp {
_updateParams(abi.decode(incoming.request.body[1:], (ParamsUpdate)));
} else if (kind == RequestKind.SweepDust) {
_sweepDust(abi.decode(incoming.request.body[1:], (SweepDust)));
} else if (kind == RequestKind.UpgradeContract) {
(address newImpl, bytes memory initData) = abi.decode(incoming.request.body[1:], (address, bytes));
ERC1967Utils.upgradeToAndCall(newImpl, initData);
}
}

Expand Down
Loading
Loading