Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion src/clob/types/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,13 @@ pub struct NotificationPayload {
#[derive(Debug, Default, Clone, Deserialize, Builder, PartialEq)]
pub struct BalanceAllowanceResponse {
pub balance: Decimal,
/// Per-spender on-chain allowances, keyed by the ERC-20 spender
/// (exchange contract) address. Values are `uint256` because the
/// typical "fully approved" state on Polymarket returns
/// `2^256 - 1`, which overflows any fixed-point decimal type.
#[serde(default)]
#[builder(default)]
pub allowances: HashMap<Address, String>,
pub allowances: HashMap<Address, U256>,
}

#[non_exhaustive]
Expand Down
42 changes: 41 additions & 1 deletion tests/clob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2261,7 +2261,7 @@ mod authenticated {

let expected = BalanceAllowanceResponse::builder()
.balance(Decimal::ZERO)
.allowances(HashMap::from_iter([(Address::ZERO, "1".to_owned())]))
.allowances(HashMap::from_iter([(Address::ZERO, U256::from(1_u64))]))
.build();

assert_eq!(response, expected);
Expand All @@ -2270,6 +2270,46 @@ mod authenticated {
Ok(())
}

#[tokio::test]
async fn balance_allowance_accepts_max_uint256_allowances() -> anyhow::Result<()> {
// Regression: MetaMask-logged-in Polymarket accounts see allowances
// equal to 2^256 - 1 after the first UI trade triggers
// `setApprovalForAll` + USDC `approve` on the exchange contracts.
// The previous `HashMap<Address, String>` type silently forced
// downstream consumers to re-parse these, and any caller that
// naively used `rust_decimal::Decimal::from_str` on the strings
// would drop the entries because `2^256 - 1` overflows Decimal.
let server = MockServer::start();
let client = create_authenticated(&server).await?;

let max_uint256 =
"115792089237316195423570985008687907853269984665640564039457584007913129639935";
let spender = Address::from([0x42; 20]);

let mock = server.mock(|when, then| {
when.method(GET).path("/balance-allowance");
then.status(StatusCode::OK).json_body(json!({
"balance": 58_916_378,
"allowances": { spender.to_string(): max_uint256 }
}));
});

let request = BalanceAllowanceRequest::builder()
.asset_type(AssetType::Collateral)
.build();
let response = client.balance_allowance(request).await?;

assert_eq!(response.balance, dec!(58_916_378));
assert_eq!(
response.allowances.get(&spender).copied(),
Some(U256::from_str(max_uint256)?)
);
assert_eq!(response.allowances.get(&spender).copied(), Some(U256::MAX));
mock.assert();

Ok(())
}

#[tokio::test]
async fn update_balance_allowance_should_succeed() -> anyhow::Result<()> {
let server = MockServer::start();
Expand Down
Loading