Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 11 additions & 3 deletions evm/script/DeployIntentGateway.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "forge-std/Script.sol";
import "stringutils/strings.sol";

import {IntentGatewayV2, Params, Deployment} 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,8 +18,15 @@ 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));
// Deploy the implementation and proxy both via CREATE2 with the same salt. The proxy
// is created with empty init data so its address depends only on (impl address, salt) —
// identical across chains. Initialization runs as a separate, deployer-gated call below.
IntentGatewayV2 implementation = new IntentGatewayV2{salt: salt}(admin);
console.log("IntentGateway implementation deployed at:", address(implementation));

ERC1967Proxy proxy = new ERC1967Proxy{salt: salt}(address(implementation), "");
Comment thread
royvardhan marked this conversation as resolved.
Outdated
IntentGatewayV2 intentGateway = IntentGatewayV2(payable(address(proxy)));
console.log("IntentGateway proxy deployed at:", address(intentGateway));
Comment thread
royvardhan marked this conversation as resolved.
Outdated

SolverAccount solverAccount = new SolverAccount{salt: salt}(address(intentGateway));
console.log("SolverAccount deployed at:", address(solverAccount));
Expand Down Expand Up @@ -77,7 +85,7 @@ contract DeployScript is BaseScript {
});
}

intentGateway.init(
intentGateway.initialize(
Params({
host: HOST_ADDRESS,
dispatcher: config.get("CALL_DISPATCHER").toAddress(),
Expand Down
34 changes: 20 additions & 14 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 Deployer authorized to call `initialize` once on the proxy. An 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 _deployer;
Comment thread
royvardhan marked this conversation as resolved.
Outdated

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

/**
Expand All @@ -87,25 +96,22 @@ 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; the deployer gate closes the front-run window left by deploying
* the proxy with empty init data. Later config changes go through `onAccept` governance.
*
* @param p The initial gateway configuration parameters.
* @param deployments The initial gateway cross-chain peers
*/
function init(Params memory p, Deployment[] memory deployments) public {
if (msg.sender != _admin) revert Unauthorized();
function initialize(Params memory p, Deployment[] memory deployments) public initializer {
if (msg.sender != _deployer) revert Unauthorized();

uint256 deploymentsLength = deployments.length;
for (uint256 i = 0; i < deploymentsLength; i++) {
_addDeployment(deployments[i]);
}
_validateParams(p);
_params = p;
_admin = address(0);
}

/**
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
15 changes: 8 additions & 7 deletions evm/src/apps/intentsv2/IntentsBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,11 @@ abstract contract IntentsBase is EIP712 {
/**
* @dev Refund escrowed tokens to the user after a cross-chain cancellation.
*/
RefundEscrow
RefundEscrow,
/**
* @dev Upgrade the gateway implementation behind its ERC-1967 proxy.
*/
UpgradeContract
}

/**
Expand All @@ -108,12 +112,6 @@ abstract contract IntentsBase is EIP712 {
*/
Params internal _params;

/**
* @dev One-time admin address set in the constructor. Has permission to call
* `setParams` exactly once, after which it is burned to address(0).
*/
address internal _admin;

/**
* @dev Maps (commitment, token address) to the escrowed amount for that token.
* Decremented as tokens are released via fills or refunds.
Expand All @@ -138,6 +136,9 @@ abstract contract IntentsBase is EIP712 {
*/
mapping(bytes32 => uint256) public _destinationProtocolFees;

/// @dev Appended last to preserve existing storage slots.
bool public _paused;

/**
* @dev Thrown when the caller is not authorized to perform the action.
*/
Expand Down
27 changes: 17 additions & 10 deletions evm/tests/foundry/IntentGatewayV2SameChainTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
CancelOptions,
Deployment
} from "../../src/apps/IntentGatewayV2.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {IntentsBase} from "../../src/apps/intentsv2/IntentsBase.sol";
import {ICallDispatcher, Call} from "@hyperbridge/core/interfaces/ICallDispatcher.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
Expand Down Expand Up @@ -63,6 +64,12 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
event EscrowRefunded(bytes32 indexed commitment, TokenInfo[] tokens);
event DustCollected(address indexed token, uint256 amount);

function _deployGatewayProxy() internal returns (IntentGatewayV2) {
IntentGatewayV2 implementation = new IntentGatewayV2(address(this));
ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), "");
return IntentGatewayV2(payable(address(proxy)));
}

function setUp() public override {
super.setUp();

Expand All @@ -72,7 +79,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
otherUser = makeAddr("otherUser");

// Deploy IntentGatewayV2
intentGateway = new IntentGatewayV2(address(this));
intentGateway = _deployGatewayProxy();

// Set params with surplus sharing but no protocol fees (to simplify tests)
Params memory intentParams = Params({
Expand All @@ -83,7 +90,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
protocolFeeBps: 0, // No protocol fees for most tests
priceOracle: address(0)
});
intentGateway.init(intentParams, new Deployment[](0));
intentGateway.initialize(intentParams, new Deployment[](0));

// Fund test accounts
_fundTestAccounts();
Expand Down Expand Up @@ -175,7 +182,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {

function testSameChainSwap_WithProtocolFee() public {
// Deploy a new gateway with protocol fees enabled
IntentGatewayV2 gatewayWithFees = new IntentGatewayV2(address(this));
IntentGatewayV2 gatewayWithFees = _deployGatewayProxy();
Params memory intentParams = Params({
host: address(host),
dispatcher: address(dispatcher),
Expand All @@ -184,7 +191,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
protocolFeeBps: PROTOCOL_FEE_BPS,
priceOracle: address(0)
});
gatewayWithFees.init(intentParams, new Deployment[](0));
gatewayWithFees.initialize(intentParams, new Deployment[](0));

uint256 inputAmount = 1000 * 1e6; // 1000 USDC
uint256 outputAmount = 900 * 1e18; // 900 DAI
Expand Down Expand Up @@ -1388,8 +1395,8 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
}

function testPartialFill_WithProtocolFee() public {
IntentGatewayV2 gatewayWithFees = new IntentGatewayV2(address(this));
gatewayWithFees.init(
IntentGatewayV2 gatewayWithFees = _deployGatewayProxy();
gatewayWithFees.initialize(
Params({
host: address(host),
dispatcher: address(dispatcher),
Expand Down Expand Up @@ -1957,7 +1964,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {

/// @notice Duplicate input tokens with protocol fees enabled must also revert.
function testRevert_PlaceOrder_DuplicateInputTokens_WithProtocolFee() public {
IntentGatewayV2 gatewayWithFees = new IntentGatewayV2(address(this));
IntentGatewayV2 gatewayWithFees = _deployGatewayProxy();
Params memory intentParams = Params({
host: address(host),
dispatcher: address(dispatcher),
Expand All @@ -1966,7 +1973,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
protocolFeeBps: PROTOCOL_FEE_BPS,
priceOracle: address(0)
});
gatewayWithFees.init(intentParams, new Deployment[](0));
gatewayWithFees.initialize(intentParams, new Deployment[](0));

TokenInfo[] memory inputs = new TokenInfo[](2);
inputs[0] = TokenInfo({token: bytes32(uint256(uint160(address(usdc)))), amount: 600 * 1e6});
Expand Down Expand Up @@ -2301,7 +2308,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {

/// @notice Fee-on-transfer with protocol fees: both deductions applied correctly.
function testPlaceOrder_FeeOnTransferToken_WithProtocolFee() public {
IntentGatewayV2 gatewayWithFees = new IntentGatewayV2(address(this));
IntentGatewayV2 gatewayWithFees = _deployGatewayProxy();
Params memory intentParams = Params({
host: address(host),
dispatcher: address(dispatcher),
Expand All @@ -2310,7 +2317,7 @@ contract IntentGatewayV2SameChainTest is MainnetForkBaseTest {
protocolFeeBps: PROTOCOL_FEE_BPS, // 30 bps
priceOracle: address(0)
});
gatewayWithFees.init(intentParams, new Deployment[](0));
gatewayWithFees.initialize(intentParams, new Deployment[](0));

FeeOnTransferToken fot = new FeeOnTransferToken(100); // 1% transfer fee
fot.mint(user, 10000 * 1e18);
Expand Down
Loading
Loading