polyrel 0.4.0

Unofficial Polymarket relayer client
Documentation
//! Polymarket V2 collateral helpers for pUSD wrap, unwrap, and CTF adapter flows.
//!
//! # Example
//!
//! ```no_run
//! use alloy_primitives::{U256, address, b256};
//! use polyrel::collateral;
//!
//! let _wrap = collateral::wrap(
//!     address!("93070a847efEf7F70739046A929D47a521F5B8ee"),
//!     address!("2791Bca1f2de4661ED88A30C99A7a9449Aa84174"),
//!     address!("6e0c80c90ea6c15917308f820eac91ce2724b5b5"),
//!     U256::from(1_000_000_u64),
//! );
//!
//! let _split = collateral::split_position(
//!     address!("ADa100874d00e3331D00F2007a9c336a65009718"),
//!     b256!("edcac9eebf21d7bf4ab3f0ceda931b194c67fa3192d6d3d3b7b74b0983e5bff1"),
//!     U256::from(1_000_000_u64),
//! );
//! ```

use alloc::vec;

use alloy_primitives::{Address, B256, Bytes, U256};
use alloy_sol_types::{SolCall, sol};

use crate::Call;

const ZERO_ADDRESS: Address = Address::ZERO;
const ZERO_BYTES32: B256 = B256::ZERO;
const PARTITION_NO: u8 = 2;
const PARTITION_YES: u8 = 1;

sol! {
	interface ICollateralOnramp {
		function wrap(address _asset, address _to, uint256 _amount) external;
	}

	interface ICollateralOfframp {
		function unwrap(address _asset, address _to, uint256 _amount) external;
	}

	interface ICtfCollateralAdapter {
		function splitPosition(
			address,
			bytes32,
			bytes32 _conditionId,
			uint256[] partition,
			uint256 _amount
		) external;

		function mergePositions(
			address,
			bytes32,
			bytes32 _conditionId,
			uint256[] partition,
			uint256 _amount
		) external;

		function redeemPositions(
			address,
			bytes32,
			bytes32 _conditionId,
			uint256[] indexSets
		) external;
	}
}

/// Builds `CollateralOnramp.wrap(asset, to, amount)` calldata.
pub fn wrap(onramp: Address, asset: Address, to: Address, amount: U256) -> Call {
	let data = Bytes::from(
		ICollateralOnramp::wrapCall { _asset: asset, _to: to, _amount: amount }.abi_encode(),
	);

	Call::builder().to(onramp).data(data).build()
}

/// Builds `CollateralOfframp.unwrap(asset, to, amount)` calldata.
pub fn unwrap(offramp: Address, asset: Address, to: Address, amount: U256) -> Call {
	let data = Bytes::from(
		ICollateralOfframp::unwrapCall { _asset: asset, _to: to, _amount: amount }.abi_encode(),
	);

	Call::builder().to(offramp).data(data).build()
}

/// Builds `CtfCollateralAdapter.splitPosition(...)` calldata for a binary market.
pub fn split_position(adapter: Address, condition_id: B256, amount: U256) -> Call {
	let data = Bytes::from(
		ICtfCollateralAdapter::splitPositionCall {
			_0: ZERO_ADDRESS,
			_1: ZERO_BYTES32,
			_conditionId: condition_id,
			partition: binary_partition(),
			_amount: amount,
		}
		.abi_encode(),
	);

	Call::builder().to(adapter).data(data).build()
}

/// Builds `CtfCollateralAdapter.mergePositions(...)` calldata for a binary market.
pub fn merge_positions(adapter: Address, condition_id: B256, amount: U256) -> Call {
	let data = Bytes::from(
		ICtfCollateralAdapter::mergePositionsCall {
			_0: ZERO_ADDRESS,
			_1: ZERO_BYTES32,
			_conditionId: condition_id,
			partition: binary_partition(),
			_amount: amount,
		}
		.abi_encode(),
	);

	Call::builder().to(adapter).data(data).build()
}

/// Builds `CtfCollateralAdapter.redeemPositions(...)` calldata for a binary market.
pub fn redeem_positions(adapter: Address, condition_id: B256) -> Call {
	let data = Bytes::from(
		ICtfCollateralAdapter::redeemPositionsCall {
			_0: ZERO_ADDRESS,
			_1: ZERO_BYTES32,
			_conditionId: condition_id,
			indexSets: binary_partition(),
		}
		.abi_encode(),
	);

	Call::builder().to(adapter).data(data).build()
}

fn binary_partition() -> alloc::vec::Vec<U256> {
	vec![U256::from(PARTITION_YES), U256::from(PARTITION_NO)]
}

#[cfg(test)]
mod tests {
	use alloy_primitives::{Bytes, U256, address, b256};

	use super::*;

	const ONRAMP: Address = address!("1111111111111111111111111111111111111111");
	const OFFRAMP: Address = address!("2222222222222222222222222222222222222222");
	const ADAPTER: Address = address!("3333333333333333333333333333333333333333");
	const ASSET: Address = address!("4444444444444444444444444444444444444444");
	const RECIPIENT: Address = address!("5555555555555555555555555555555555555555");
	const CONDITION_ID: B256 =
		b256!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
	const AMOUNT: u64 = 42;
	const WRAP_EXPECTED: &str = "6235563800000000000000000000000044444444444444444444444444444444444444440000000000000000000000005555555555555555555555555555555555555555000000000000000000000000000000000000000000000000000000000000002a";
	const UNWRAP_EXPECTED: &str = "8cc7104f00000000000000000000000044444444444444444444444444444444444444440000000000000000000000005555555555555555555555555555555555555555000000000000000000000000000000000000000000000000000000000000002a";
	const SPLIT_EXPECTED: &str = "72ce427500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002";
	const MERGE_EXPECTED: &str = "9e7212ad00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002";
	const REDEEM_EXPECTED: &str = "01b7037c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002";
	const VALID_CALLDATA_FIXTURE: &str = "valid calldata fixture";

	#[test]
	fn wrap_encodes_expected_calldata() {
		// Arrange
		let expected = bytes(WRAP_EXPECTED);

		// Act
		let call = wrap(ONRAMP, ASSET, RECIPIENT, U256::from(AMOUNT));

		// Assert
		assert_eq!(call.to(), ONRAMP);
		assert_eq!(call.value(), U256::ZERO);
		assert_eq!(call.data(), &expected);
	}

	#[test]
	fn unwrap_encodes_expected_calldata() {
		// Arrange
		let expected = bytes(UNWRAP_EXPECTED);

		// Act
		let call = unwrap(OFFRAMP, ASSET, RECIPIENT, U256::from(AMOUNT));

		// Assert
		assert_eq!(call.to(), OFFRAMP);
		assert_eq!(call.value(), U256::ZERO);
		assert_eq!(call.data(), &expected);
	}

	#[test]
	fn split_position_encodes_expected_calldata() {
		// Arrange
		let expected = bytes(SPLIT_EXPECTED);

		// Act
		let call = split_position(ADAPTER, CONDITION_ID, U256::from(AMOUNT));

		// Assert
		assert_eq!(call.to(), ADAPTER);
		assert_eq!(call.value(), U256::ZERO);
		assert_eq!(call.data(), &expected);
	}

	#[test]
	fn merge_positions_encodes_expected_calldata() {
		// Arrange
		let expected = bytes(MERGE_EXPECTED);

		// Act
		let call = merge_positions(ADAPTER, CONDITION_ID, U256::from(AMOUNT));

		// Assert
		assert_eq!(call.to(), ADAPTER);
		assert_eq!(call.value(), U256::ZERO);
		assert_eq!(call.data(), &expected);
	}

	#[test]
	fn redeem_positions_encodes_expected_calldata() {
		// Arrange
		let expected = bytes(REDEEM_EXPECTED);

		// Act
		let call = redeem_positions(ADAPTER, CONDITION_ID);

		// Assert
		assert_eq!(call.to(), ADAPTER);
		assert_eq!(call.value(), U256::ZERO);
		assert_eq!(call.data(), &expected);
	}

	fn bytes(hex: &str) -> Bytes {
		let decoded = alloy_primitives::hex::decode(hex).expect(VALID_CALLDATA_FIXTURE);

		Bytes::from(decoded)
	}
}