Skip to content

Commit 52f503f

Browse files
KitHatbidzyyys
andauthored
Add XCM Escrow Input Settler for Polkadot Cross-Chain Intent Settlement (#7)
* contracts, mocks and tests * Added reentrancy guards, added XCM hand brake, minor optimizations * Fixes suggested by MCP * Added docs to the contract functions Added docs to the XCM message building library Fixed review comments * More review fixes * applied more comments --------- Co-authored-by: Daniel Bigos <daniel.bigos96@gmail.com>
1 parent 98fda55 commit 52f503f

12 files changed

+1646
-0
lines changed

contracts/InputSettlerXCMEscrow.sol

Lines changed: 494 additions & 0 deletions
Large diffs are not rendered by default.

contracts/interfaces/ILibrary.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
/**
4+
* @title ILibrary
5+
* @notice Interface for libraries that build XCM messages.
6+
* Implemented in ink! and deployed in PolkaVM, this library currently supports constructing Teleport messages only.
7+
* XCM messages are returned as SCALE-encoded bytes for use with XCM precompiles.
8+
*
9+
* @dev Example:
10+
* bytes memory xcmMsg = ILibrary(inkLibrary).teleport(paraId, beneficiary, amount);
11+
*/
12+
interface ILibrary {
13+
function teleport(
14+
uint32 paraId,
15+
bytes32 beneficiary,
16+
uint128 amount
17+
) external returns (bytes memory);
18+
}

contracts/interfaces/IXcm.sol

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
/// @dev The on-chain address of the XCM (Cross-Consensus Messaging) precompile.
5+
address constant XCM_PRECOMPILE_ADDRESS = address(0xA0000);
6+
7+
/// @title XCM Precompile Interface
8+
/// @notice A low-level interface for interacting with `pallet_xcm`.
9+
/// It forwards calls directly to the corresponding dispatchable functions,
10+
/// providing access to XCM execution and message passing.
11+
/// @dev Documentation:
12+
/// @dev - XCM: https://docs.polkadot.com/develop/interoperability
13+
/// @dev - SCALE codec: https://docs.polkadot.com/polkadot-protocol/parachain-basics/data-encoding
14+
/// @dev - Weights: https://docs.polkadot.com/polkadot-protocol/parachain-basics/blocks-transactions-fees/fees/#transactions-weights-and-fees
15+
interface IXcm {
16+
/// @notice Weight v2 used for measurement for an XCM execution
17+
struct Weight {
18+
/// @custom:property The computational time used to execute some logic based on reference hardware.
19+
uint64 refTime;
20+
/// @custom:property The size of the proof needed to execute some logic.
21+
uint64 proofSize;
22+
}
23+
24+
/// @notice Executes an XCM message locally on the current chain with the caller's origin.
25+
/// @dev Internally calls `pallet_xcm::execute`.
26+
/// @param message A SCALE-encoded Versioned XCM message.
27+
/// @param weight The maximum allowed `Weight` for execution.
28+
/// @dev Call @custom:function weighMessage(message) to ensure sufficient weight allocation.
29+
function execute(bytes calldata message, Weight calldata weight) external;
30+
31+
/// @notice Sends an XCM message to another parachain or consensus system.
32+
/// @dev Internally calls `pallet_xcm::send`.
33+
/// @param destination SCALE-encoded destination MultiLocation.
34+
/// @param message SCALE-encoded Versioned XCM message.
35+
function send(bytes calldata destination, bytes calldata message) external;
36+
37+
/// @notice Estimates the `Weight` required to execute a given XCM message.
38+
/// @param message SCALE-encoded Versioned XCM message to analyze.
39+
/// @return weight Struct containing estimated `refTime` and `proofSize`.
40+
function weighMessage(bytes calldata message) external view returns (Weight memory weight);
41+
}

contracts/test/MockERC20.sol

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
5+
6+
contract MockERC20 is ERC20 {
7+
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
8+
9+
function mint(address to, uint256 amount) public {
10+
_mint(to, amount);
11+
}
12+
13+
function burn(address from, uint256 amount) public {
14+
_burn(from, amount);
15+
}
16+
}

contracts/test/MockLibrary.sol

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import "../interfaces/ILibrary.sol";
5+
import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol";
6+
import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
7+
8+
contract MockLibrary is ILibrary {
9+
event TeleportCalled(uint32 paraId, bytes32 beneficiary, uint128 amount);
10+
11+
bytes private teleportMessage = "0x";
12+
address private tokenAddress;
13+
14+
function setTeleportMessage(bytes memory message) external {
15+
teleportMessage = message;
16+
}
17+
18+
function setToken(address _token) external {
19+
tokenAddress = _token;
20+
}
21+
22+
function teleport(
23+
uint32 paraId,
24+
bytes32 beneficiary,
25+
uint128 amount
26+
) external returns (bytes memory) {
27+
// Simulate burning tokens by transferring from caller to this contract
28+
if (tokenAddress != address(0)) {
29+
SafeERC20.safeTransferFrom(IERC20(tokenAddress), msg.sender, address(this), amount);
30+
}
31+
emit TeleportCalled(paraId, beneficiary, amount);
32+
return teleportMessage;
33+
}
34+
}

contracts/test/MockXcm.sol

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import "../interfaces/IXcm.sol";
5+
6+
contract MockXcm is IXcm {
7+
event Executed(bytes message);
8+
event Sent(bytes destination, bytes message);
9+
event WeighMessageCalled(bytes message);
10+
11+
bool private executionSuccess = true;
12+
Weight private mockWeight = Weight({refTime: 1000000, proofSize: 1000});
13+
14+
function setExecutionSuccess(bool success) external {
15+
executionSuccess = success;
16+
}
17+
18+
function setMockWeight(uint64 refTime, uint64 proofSize) external {
19+
mockWeight = Weight({refTime: refTime, proofSize: proofSize});
20+
}
21+
22+
function execute(
23+
bytes calldata message,
24+
Weight calldata weight
25+
) external override {
26+
require(executionSuccess, "MockXcm: execution failed");
27+
emit Executed(message);
28+
}
29+
30+
function send(
31+
bytes calldata destination,
32+
bytes calldata message
33+
) external override {
34+
emit Sent(destination, message);
35+
}
36+
37+
function weighMessage(
38+
bytes calldata message
39+
) external view override returns (Weight memory weight) {
40+
return mockWeight;
41+
}
42+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
2+
3+
const InputSettlerXCMEscrowModule = buildModule("InputSettlerXCMEscrowModule", (m) => {
4+
// Parameters with defaults
5+
// Default XCM precompile address on many Polkadot parachains is 0xA0000 or similar precompiles
6+
const xcmPrecompile = m.getParameter("xcmPrecompile", "0x00000000000000000000000000000000000A0000");
7+
8+
// For inkLibrary, we might need a real address. Defaulting to zero address for now if not provided.
9+
// User should provide this when deploying to a real network.
10+
const inkLibrary = m.getParameter("inkLibrary", "0x0000000000000000000000000000000000000000");
11+
12+
// Deploy the base settler contract
13+
// Note: InputSettlerEscrow is imported from oif-contracts
14+
const baseSettler = m.contract("InputSettlerEscrow", []);
15+
16+
// Deploy the XCM settler
17+
const inputSettlerXCMEscrow = m.contract("InputSettlerXCMEscrow", [
18+
inkLibrary,
19+
xcmPrecompile,
20+
baseSettler
21+
]);
22+
23+
return { baseSettler, inputSettlerXCMEscrow };
24+
});
25+
26+
export default InputSettlerXCMEscrowModule;
27+
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const { expect } = require("chai");
2+
const { ethers } = require("hardhat");
3+
const {
4+
setupInputSettlerXCMEscrow,
5+
createOrderFactory,
6+
DESTINATION_CHAIN_ID,
7+
STANDARD_AMOUNT,
8+
DOUBLE_AMOUNT,
9+
MOCK_XCM_MESSAGE_1
10+
} = require("./helpers/inputSettlerXCMEscrowHelper");
11+
12+
describe("InputSettlerXCMEscrow - Admin Functions", function () {
13+
let inputSettlerXCMEscrow;
14+
let token;
15+
let owner;
16+
let user;
17+
let mockXcm;
18+
let mockLibrary;
19+
let baseSettler;
20+
let chainId;
21+
let createOrder;
22+
23+
beforeEach(async function () {
24+
const setup = await setupInputSettlerXCMEscrow();
25+
owner = setup.owner;
26+
user = setup.user;
27+
mockXcm = setup.mockXcm;
28+
mockLibrary = setup.mockLibrary;
29+
baseSettler = setup.baseSettler;
30+
inputSettlerXCMEscrow = setup.inputSettlerXCMEscrow;
31+
token = setup.token;
32+
chainId = setup.chainId;
33+
createOrder = createOrderFactory(user, token, chainId);
34+
});
35+
36+
describe("Deployment", function () {
37+
it("Should set the right owner", async function () {
38+
expect(await inputSettlerXCMEscrow.owner()).to.equal(owner.address);
39+
});
40+
});
41+
42+
describe("allowTeleport", function () {
43+
it("Should allow teleport for a destination and token", async function () {
44+
const tokenAddress = await token.getAddress();
45+
46+
await expect(inputSettlerXCMEscrow.allowTeleport(DESTINATION_CHAIN_ID, tokenAddress))
47+
.to.emit(inputSettlerXCMEscrow, "TeleportAllowed")
48+
.withArgs(DESTINATION_CHAIN_ID, tokenAddress);
49+
});
50+
51+
it("Should revert if not called by owner", async function () {
52+
const tokenAddress = await token.getAddress();
53+
54+
await expect(
55+
inputSettlerXCMEscrow.connect(user).allowTeleport(DESTINATION_CHAIN_ID, tokenAddress)
56+
).to.be.revertedWithCustomError(inputSettlerXCMEscrow, "OwnableUnauthorizedAccount")
57+
.withArgs(user.address);
58+
});
59+
});
60+
61+
describe("forbidTeleport", function () {
62+
it("Should forbid teleport for a destination and token", async function () {
63+
const tokenAddress = await token.getAddress();
64+
65+
await inputSettlerXCMEscrow.allowTeleport(DESTINATION_CHAIN_ID, tokenAddress);
66+
67+
await expect(inputSettlerXCMEscrow.forbidTeleport(DESTINATION_CHAIN_ID, tokenAddress))
68+
.to.emit(inputSettlerXCMEscrow, "TeleportForbidden")
69+
.withArgs(DESTINATION_CHAIN_ID, tokenAddress);
70+
});
71+
});
72+
73+
describe("setXCMEnabled", function () {
74+
it("Should fall back to baseSettler when XCM is disabled for valid XCM order", async function () {
75+
await inputSettlerXCMEscrow.allowTeleport(DESTINATION_CHAIN_ID, await token.getAddress());
76+
77+
const order = createOrder();
78+
79+
// Verify XCM path would work normally
80+
await token.connect(user).approve(await inputSettlerXCMEscrow.getAddress(), ethers.parseEther(DOUBLE_AMOUNT));
81+
await mockLibrary.setTeleportMessage(MOCK_XCM_MESSAGE_1);
82+
await mockXcm.setExecutionSuccess(true);
83+
84+
// Disable XCM globally
85+
await inputSettlerXCMEscrow.setXCMEnabled(false);
86+
87+
// Should fall back to baseSettler despite valid XCM configuration
88+
await expect(inputSettlerXCMEscrow.connect(user).open(order))
89+
.to.emit(baseSettler, "Open");
90+
91+
// Verify XCM was not called
92+
const xcmAllowance = await token.allowance(
93+
await inputSettlerXCMEscrow.getAddress(),
94+
await mockXcm.getAddress()
95+
);
96+
expect(xcmAllowance).to.equal(0);
97+
});
98+
99+
it("Should resume XCM path when re-enabled", async function () {
100+
await inputSettlerXCMEscrow.allowTeleport(DESTINATION_CHAIN_ID, await token.getAddress());
101+
102+
const order = createOrder();
103+
104+
await token.connect(user).approve(await inputSettlerXCMEscrow.getAddress(), ethers.parseEther(STANDARD_AMOUNT));
105+
await mockLibrary.setTeleportMessage(MOCK_XCM_MESSAGE_1);
106+
await mockXcm.setExecutionSuccess(true);
107+
108+
// Disable then re-enable XCM
109+
await inputSettlerXCMEscrow.setXCMEnabled(false);
110+
await inputSettlerXCMEscrow.setXCMEnabled(true);
111+
112+
// Should use XCM path again
113+
await expect(inputSettlerXCMEscrow.connect(user).open(order))
114+
.to.emit(mockXcm, "Executed")
115+
.withArgs(MOCK_XCM_MESSAGE_1);
116+
});
117+
118+
it("Should revert if not called by owner", async function () {
119+
await expect(
120+
inputSettlerXCMEscrow.connect(user).setXCMEnabled(false)
121+
).to.be.revertedWithCustomError(inputSettlerXCMEscrow, "OwnableUnauthorizedAccount")
122+
.withArgs(user.address);
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)