Backend Integrations
Programmatically deposit into and withdraw from SyrupUSD
Step-by-step
Deposit:
Query the Maple API to retrieve the necessary contract addresses.
Determine user authorization as a Syrup lender.
Retrieve an authorization signature from the Maple API
Execute the Deposit (authorize and deposit, or deposit only).
Withdrawal:
Retrieve Pool Position from the Maple API
Calculate Shares to Redeem from the Pool contract
Execute the Withdrawal on the Pool contract
SDK and GraphQL API
All necessary ABIs and addresses are available in the Maple SDK or via GitHub:
Within the SDK, both ABIs and network-specific addresses are accessible. The package can also be installed and used within applications.
npm install @maplelabs/maple-js
To access the necessary data, use the GraphQL API:
https://api.maple.finance/v2/graphql
NOTE: In order to perform the integration in Sepolia, you'll need to contact us via Telegram to receive Sepolia USDC
/USDT
tokens.
Deposit
1. Query the Maple API
Syrup's main contract, the SyrupRouter
, is designed to allow authorized participants to securely access the yields available in the Maple ecosystem, abstracting all the complexity of the permissioning system. Each Syrup PoolV2
has an associated SyrupRouter
.
Syrup USDC
SyrupRouter
WithdrawalManagerQueue
Syrup USDT
SyrupRouter
WithdrawalManagerQueue
Router addresses can also be accessed via the GraphQL API.
Example query
query {
poolV2S(where: { syrupRouter_not: null }) {
id
asset {
symbol
}
syrupRouter {
id
}
withdrawalManagerQueue {
id
}
}
}
Code example using graphql-request
import { gql, GraphQLClient } from "graphql-request";
interface MaplePoolV2 {
id: string;
asset: {
symbol: string;
};
syrupRouter: {
id: string;
};
withdrawalManagerQueue {
id: string;
}
}
interface QueryResponse {
poolV2S: MaplePoolV2[];
}
const query = gql`
query GetMaplePools {
poolV2S(where: { syrupRouter_not: null }) {
id
name
asset {
symbol
}
syrupRouter {
id
}
withdrawalManagerQueue {
id
}
}
}
`;
const client = new GraphQLClient(MAPLE_API_URL);
const main = async () => {
const { poolV2S } = await client.request<QueryResponse>(query);
for (const poolV2 of poolV2S) {
console.log(`Pool ${poolV2.name}: SyrupRouter: ${poolV2.syrupRouter.id} WithdrawalManagerQueue: ${poolV2.withdrawalManagerQueue.id}`);
}
};
main();
It is important to note that the query uses the syrupRouter_not
filter to return specifically Syrup pools.
Response Fields:
poolV2S.id
: The Syrup Pool contract address.poolV2S.syrupRouter.id
: TheSyrupRouter
contract address.
These addresses can then be used to interact with the SyrupRouter
contract.
2. Determine User Authorization
Before depositing, check if a user is already authorized by querying the Account
entity. This is necessary to perform a deposit.
Example Query
query GetMapleAccount($accountId: ID!) {
account(id: $accountId) {
isSyrupLender
}
}
NOTE: The account ID must be provided in lowercase.
Code example using graphql-request
import { gql, GraphQLClient } from "graphql-request";
interface MapleAccount {
isSyrupLender: boolean;
}
interface QueryResponse {
account: MapleAccount;
}
const query = gql`
query GetMapleAccount($accountId: ID!) {
account(id: $accountId) {
isSyrupLender
}
}
`;
const client = new GraphQLClient(MAPLE_API_URL);
const main = async () => {
const account = "0x123...";
const { account: mapleAccount } = await client.request<QueryResponse>(query, {
accountId: account.toLowerCase(),
});
if (mapleAccount) {
console.log(`Can user ${account} deposit into Syrup: ${mapleAccount.isSyrupLender}`);
} else {
console.log("Account not found");
}
};
main();
If isSyrupLender = true
, the user is already authorized in the Pool Permission Manager
and can deposit into Syrup pools. Otherwise, the user must perform authorization before their initial deposit.
3. Retrieve Authorization Signature
Note: This step is only required if isSyrupLender = false
was returned in the previous step. Otherwise, continue to the next step.
The Maple Protocol is geared towards institutions and has a permissioning system that requires allowlisting for executing most functions. For pool deposits, in general, lenders need to have their wallet allowlisted in Maple's Pool Permission Manager
. Aiming to abstract and simplify the process, the SyrupRouter
integrates directly with the Pool Permission Manager
to allow for valid users to self-authorize and deposit in a single transaction assuming the user meets eligibility requirements.
To retrieve the authorization signature, contact the Syrup team via Telegram at https://t.me/syrupfi.
4. Execute the Deposit
The first time an asset (e.g., USDC
, USDT
) is lent to Syrup, it may be necessary to allow the contract to interact with the asset. This is a common transaction on Ethereum.
Depositing into a pool requires a transaction. Once the transaction has been processed, SyrupRouter
will accept the lending tokens and Pool LP (Liquidity Provider) tokens will be received.
Each pool contract inherits the ERC-4626 standard, also known as the Tokenized Vault
Standard. This standard informs how the LP Tokens accrue value from borrower repayments of loans.
isSyrupLender = true
- User is Already Authorized
isSyrupLender = true
- User is Already AuthorizedThe deposit
or depositWithPermit
method can be called directly on SyrupRouter
.
function deposit(uint256 assets, bytes32 depositData)
NOTE: depositData
is an optional field that does not need to be provided.
Code example using Maple SDK for USDC
or USDT
.
USDC
or USDT
.import { BigNumber, Contract, providers, utils, Wallet } from "ethers";
import { addresses, syrupUtils } from "@maplelabs/maple-js";
const main = async () => {
const provider = new providers.JsonRpcProvider(RPC_URL);
const signer = new Wallet(PRIVATE_KEY, provider);
const syrupRouter = syrupUtils.syrupRouter.connect(
SYRUP_USDC_ROUTER_ADDRESS, // Explained how to get in previous step
signer
);
const usdc = new Contract(addresses["mainnet-prod"].USDC, USDC_ABI, signer);
const amount = BigNumber.from(1000000);
const approveReceipt = await usdc.approve(SYRUP_USDC_ROUTER_ADDRESS, amount);
await approveReceipt.wait();
const depositReceipt = await syrupRouter.deposit(
amount,
utils.formatBytes32String("")
);
await depositReceipt.wait();
};
main();
Deposits into Syrup can also be made with gasless approval using permit
. For more information, see https://eips.ethereum.org/EIPS/eip-2612.
function depositWithPermit(
uint256 amount,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s,
bytes32 depositData_
)
NOTE: depositWithPermit
is only available for Syrup USDC
.
Code example using Maple SDK
import { BigNumber, Contract, providers, utils, Wallet } from "ethers";
import { addresses, syrupUtils, erc20 } from "@maplelabs/maple-js";
const main = async () => {
const provider = new providers.JsonRpcProvider(RPC_URL);
const signer = new Wallet(PRIVATE_KEY, provider);
const syrupRouter = syrupUtils.syrupRouter.connect(
SYRUP_USDC_ROUTER_ADDRESS, // Explained how to get in previous step
signer
);
const usdc = new Contract(addresses["mainnet-prod"].USDC, USDC_ABI, signer);
const amount = BigNumber.from(1000000);
const account = await signer.getAddress();
const deadline = Math.floor(Date.now() / 1000) + 3600;
const nonce = await usdc.nonces(account);
const network = await provider.getNetwork();
// EIP-712 domain for USDC
const domain = {
name: await usdc.name(),
version: "2", // "1" for Sepolia
chainId: network.chainId,
verifyingContract: addresses["mainnet-prod"].USDC,
};
// EIP-712 types for permit
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const permitMessage = {
owner: account,
spender: SYRUP_USDC_ROUTER_ADDRESS,
value: amount.toString(),
nonce: nonce.toString(),
deadline: deadline,
};
const signature = await signer._signTypedData(domain, types, permitMessage);
const sig = utils.splitSignature(signature);
const depositWithPermitReceipt = await syrupRouter.depositWithPermit(
amount,
deadline,
sig.v,
sig.r,
sig.s,
utils.formatBytes32String("")
);
await depositWithPermitReceipt.wait();
};
main();
isSyrupLender = false
- User Requires Authorization
isSyrupLender = false
- User Requires AuthorizationRetrieve a signature from the Maple API. Follow the instructions received from the team as mentioned in the step above.
Use the retrieved signature with the
authorizeAndDeposit
orauthorizeAndDepositWithPermit
method onSyrupRouter
.
function authorizeAndDeposit(
uint256 bitmap,
uint256 deadline,
uint8 auth_v,
bytes32 auth_r,
bytes32 auth_s,
uint256 amount,
bytes32 depositData)
Code example using Maple SDK for USDC
or USDT
.
USDC
or USDT
.import { BigNumber, providers, utils, Wallet } from "ethers";
import { addresses, syrupUtils } from "@maplelabs/maple-js";
const main = async () => {
... // Exact same steps as regular deposit
const { bitmap, deadline, sig } = authorize(); // Contact the Syrup team for authorization information
const authorizeAndDepositReceipt = await syrupRouter.authorizeAndDeposit(
bitmap,
deadline,
sig.v,
sig.r,
sig.s,
amount,
utils.formatBytes32String("")
);
await authorizeAndDepositReceipt.wait();
};
main();
Deposits into Syrup can also be made with gasless approval using permit
. For more information, see https://eips.ethereum.org/EIPS/eip-2612.
function authorizeAndDepositWithPermit(
uint256 bitmap,
uint256 auth_deadline,
uint8 auth_v,
bytes32 auth_r,
bytes32 auth_s,
uint256 amount,
bytes32 depositData,
uint256 permit_deadline,
uint8 permit_v,
bytes32 permit_r,
bytes32 permit_s
)
NOTE: authorizeAndDepositWithPermit
is only available for Syrup USDC
.
Code example using Maple SDK
import { BigNumber, providers, utils, Wallet } from "ethers";
import { addresses, syrupUtils } from "@maplelabs/maple-js";
const main = async () {
... // Exact same steps as regular depositWithPermit
const { bitmap, authDeadline, authSig } = authorize(); // Contact the Syrup team for authorization information
const authorizeAndDepositWithPermitReceipt = await syrupRouter.authorizeAndDepositWithPermit(
bitmap,
authDeadline,
authSig.v,
authSig.r,
authSig.s,
amount,
utils.formatBytes32String(''),
deadline,
sig.v,
sig.r,
sig.s
);
await authorizeAndDepositWithPermitReceipt.wait();
}
main();
Withdraw
1. Retrieve Pool Position
Query the Maple API for the user's pool position data using the PoolV2Position
field in the Account
query:
Data model
PoolPositionV2 {
availableBalance // The pool position in the pool asset.
availableShares // The underlying shares representing that position.
redeemRequested // A boolean indicating if a redeem request is active.
}
Example query
query GetMapleAccount($accountId: ID!, $poolId: String!) {
account(id: $accountId) {
id
poolV2Positions(where: { pool: $poolId }) {
id // "$lender_address-$pool_address"
availableBalance // Position in pool asset
availableShares // Underlying shares
redeemRequested // Boolean flag
pool {
name
}
}
}
}
Code example using graphql-request
import { gql, GraphQLClient } from "graphql-request";
interface MaplePoolPosition {
id: string;
availableBalance: string;
availableShares: string;
redeemRequested: boolean;
pool: {
name: string;
};
}
interface MapleAccount {
id: string;
poolV2Positions: MaplePoolPosition[];
}
interface QueryResponse {
account: MapleAccount;
}
const query = gql`
query GetMapleAccount($accountId: ID!, $poolId: String!) {
account(id: $accountId) {
id
poolV2Positions(where: { pool: $poolId }) {
id
availableBalance
availableShares
redeemRequested
pool {
name
}
}
}
}
`;
const client = new GraphQLClient(MAPLE_API_URL);
const main = async () => {
const account = "0x123...";
const { account: mapleAccount } = await client.request<QueryResponse>(query, {
accountId: account.toLowerCase(),
poolId: SYRUP_USDC,
});
if (mapleAccount) {
for (const poolV2Position of mapleAccount.poolV2Positions) {
console.log(`Pool ${poolV2Position.pool.name} position for ${account}`);
console.log(`Available balance: ${poolV2Position.availableBalance}`);
console.log(`Available shares: ${poolV2Position.availableShares}`);
}
} else {
console.log("Account not found");
}
};
main();
2. Calculate Shares to Redeem
Withdrawal requests must be expressed in shares. Although the Maple API provides both
availableShares
andavailableBalance
, losses or impairments on the pool may affect the value of assets relative to shares.To ensure accuracy, convert the desired asset amount to "exit shares" using the pool contract's conversion method.
These transactions leverage the
ERC-4626
tokenized vault standard. For more information, see https://ethereum.org/en/developers/docs/standards/tokens/erc-4626/.
Function signature
function convertToExitShares(uint256 assets_) external view returns (uint256 shares_);
Code example using Maple SDK
import { BigNumber, providers } from "ethers";
import { poolV2 } from "@maplelabs/maple-js";
const main = async () => {
const provider = new providers.JsonRpcProvider(RPC_URL);
const syrupPool = poolV2.core.connect(SYRUP_USDC, provider);
const amount = BigNumber.from(10 ** 8);
const sharesToRedeem = await syrupPool.convertToExitShares(amount);
console.log(`${amount.toString()} is equal to ${sharesToRedeem.toString()}`);
};
main();
3. Execute the Withdrawal
After calculating sharesToRedeem
or fetching availableShares
, call the requestRedeem
method on the Pool contract to initiate the withdrawal.
Function signature
function requestRedeem(
uint256 shares, // Shares to redeem (from Step 1 or Step 2)
address receiver // Address to receive the assets
)
Code example using Maple SDK
import { BigNumber, providers, Wallet } from "ethers";
import { poolV2 } from "@maplelabs/maple-js";
const main = async () => {
const provider = new providers.JsonRpcProvider(RPC_URL);
const signer = new Wallet(PRIVATE_KEY, provider);
const syrupPool = poolV2.core.connect(SYRUP_USDC_POOL, signer);
const account = await signer.getAddress();
const requestRedeemReceipt = await syrupPool.requestRedeem(
sharesToRedeem,
account
);
await requestRedeemReceipt.wait();
};
main();
4. Await Withdrawal Completion
Once the transaction is successful and there is sufficient liquidity in the pool, the withdrawal will be processed within a few minutes. The current status of the withdrawal queue can be retrieved either directly from the WithdrawalManagerQueue
contract or through the Maple GraphQL API:
Example query
query GetPoolV2Queue($id: ID!) {
poolV2(id: $id) {
withdrawalManagerQueue {
totalShares
nextRequest {
id
shares
status
}
}
}
}
Code example using graphql-request
import { gql, GraphQLClient } from "graphql-request";
interface MapleWithdrawalManagerQueue {
totalShares: string;
nextRequest: {
id: string;
shares: string;
status: string;
};
}
interface MaplePool {
withdrawalManagerQueue: MapleWithdrawalManagerQueue;
}
interface QueryResponse {
poolV2: MaplePool;
}
const query = gql`
query GetPoolV2Queue($poolId: ID!) {
poolV2(id: $poolId) {
withdrawalManagerQueue {
totalShares
nextRequest {
id
shares
status
}
}
}
}
`;
const client = new GraphQLClient(MAPLE_API_URL);
const main = async () => {
const { poolV2 } = await client.request<QueryResponse>(query, {
poolId: SYRUP_USDC,
});
if (poolV2) {
const { withdrawalManagerQueue } = poolV2;
console.log(
`Total shares in the queue ${withdrawalManagerQueue.totalShares}`
);
console.log(
`Next request ${withdrawalManagerQueue.nextRequest.id}: ${withdrawalManagerQueue.nextRequest.shares} shares. ${withdrawalManagerQueue.nextRequest.status}`
);
}
};
main();
Edge Cases
FAQ
Last updated