Skip to content
3 changes: 0 additions & 3 deletions contracts/evmx/base/AppGatewayBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@ abstract contract AppGatewayBase is AddressResolverUtil, IAppGateway {
// slot 54
OverrideParams public overrideParams;

// slot 55
bytes public onCompleteData;

// slot 57
mapping(address => bool) public isValidPromise;

Expand Down
159 changes: 153 additions & 6 deletions contracts/evmx/fees/Credit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import "../interfaces/IFeesPool.sol";

import {AddressResolverUtil} from "../helpers/AddressResolverUtil.sol";
import {NonceUsed, InvalidAmount, InsufficientCreditsAvailable, InsufficientBalance, InvalidChainSlug, NotRequestHandler, InvalidReceiver} from "../../utils/common/Errors.sol";
import {WRITE, FAST} from "../../utils/common/Constants.sol";
import {WRITE, CHAIN_SLUG_SOLANA_MAINNET} from "../../utils/common/Constants.sol";
import "../../utils/RescueFundsLib.sol";
import "../base/AppGatewayBase.sol";
import {toBytes32Format} from "../../utils/common/Converters.sol";
import {ForwarderSolana} from "../helpers/ForwarderSolana.sol";
import {SolanaInstruction, SolanaInstructionData, SolanaInstructionDataDescription} from "../../utils/common/Structs.sol";
import {FeesPlugProgramPda} from "../helpers/solana-utils/program-pda/FeesPlugPdas.sol";
import {SolanaPDA} from "../helpers/solana-utils/SolanaPda.sol";
import {TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID} from "../helpers/solana-utils/SolanaPda.sol";

abstract contract FeesManagerStorage is IFeesManager {
// slots [0-49] reserved for gap
Expand All @@ -40,7 +45,7 @@ abstract contract FeesManagerStorage is IFeesManager {
// slot 53
// token pool balances
// chainSlug => token address => amount
mapping(uint32 => mapping(address => uint256)) public tokenOnChainBalances;
mapping(uint32 => mapping(bytes32 => uint256)) public tokenOnChainBalances;

// slot 54
/// @notice Mapping to track nonce to whether it has been used
Expand All @@ -58,10 +63,15 @@ abstract contract FeesManagerStorage is IFeesManager {
/// @dev chainSlug => max fees
mapping(uint32 => uint256) public maxFeesPerChainSlug;

// slots [57-106] reserved for gap
uint256[50] _gap_after;
ForwarderSolana public forwarderSolana;

// slots [107-156] 50 slots reserved for address resolver util
bytes32 public susdcSolanaProgramId;
bytes32 public feesPlugSolanaProgramId;

// slots [60-107] reserved for gap
uint256[44] _gap_after;

// slots [108-157] 50 slots reserved for address resolver util
// 9 slots for app gateway base
}

Expand Down Expand Up @@ -99,6 +109,9 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew
/// @notice Emitted when withdraw fails
event WithdrawFailed(bytes32 indexed payloadId);

/// @notice Emitted when fees plug solana program id is set
event FeesPlugSolanaSet(bytes32 indexed feesPlugSolanaProgramId);

function setFeesPlug(uint32 chainSlug_, bytes32 feesPlug_) external onlyOwner {
feesPlugs[chainSlug_] = feesPlug_;
emit FeesPlugSet(chainSlug_, feesPlug_);
Expand Down Expand Up @@ -245,7 +258,7 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew

// Burn tokens from sender
_burn(consumeFrom, credits_);
tokenOnChainBalances[chainSlug_][token_] -= credits_;
tokenOnChainBalances[chainSlug_][toBytes32Format(token_)] -= credits_;

// Add it to the queue and submit request
_createRequest(
Expand All @@ -256,6 +269,44 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew
);
}

function withdrawCreditsSolana(
uint32 chainSlug_,
bytes32 token_,
uint256 credits_,
uint256 maxFees_,
bytes32 onchainReceiver_
) public async {
// sender is evmx address (credit holder) that is making the call (it is will be AG in case of Game)
address consumeFrom = msg.sender;

// Check if amount is available in fees plug
uint256 availableCredits = balanceOf(consumeFrom);
if (availableCredits < credits_ + maxFees_) revert InsufficientCreditsAvailable();

// Burn tokens from sender
_burn(consumeFrom, credits_);
tokenOnChainBalances[chainSlug_][token_] -= credits_;

feesPlugWithdrawSolana(token_, credits_, onchainReceiver_);
}

function feesPlugWithdrawSolana(
bytes32 token_,
uint256 credits_,
bytes32 onchainReceiver_
) internal {
SolanaInstruction memory solanaInstruction_ = createFeesPlugWithdrawInstructionSolana(
onchainReceiver_,
token_,
credits_
);
forwarderSolana.callSolana(
abi.encode(solanaInstruction_),
solanaInstruction_.data.programId,
address(this)
);
}

function _createRequest(
uint32 chainSlug_,
address consumeFrom_,
Expand Down Expand Up @@ -306,4 +357,100 @@ abstract contract Credit is FeesManagerStorage, Initializable, Ownable, AppGatew
function decimals() public pure override returns (uint8) {
return 18;
}

function createFeesPlugWithdrawInstructionSolana(
bytes32 userAddress_,
bytes32 susdcMint_,
uint256 withdrawAmount_
) internal view returns (SolanaInstruction memory) {
bytes32[] memory accounts = new bytes32[](11);
// accounts 0 - tmpReturnStoragePda
(accounts[0], ) = FeesPlugProgramPda.deriveTmpReturnStoragePda(feesPlugSolanaProgramId);
/*----------------- mint() accounts -----------------*/
// accounts 1 - programConfigPda
(accounts[1], ) = FeesPlugProgramPda.deriveProgramConfigPda(feesPlugSolanaProgramId);
// accounts 2 - vaultConfigPda
(bytes32 vaultConfigPda, ) = FeesPlugProgramPda.deriveVaultConfigPda(
feesPlugSolanaProgramId
);
accounts[2] = vaultConfigPda;
// accounts 3 - token mint address
accounts[3] = susdcMint_;
// accounts 4 - whitelistedTokenPda
(accounts[4], ) = FeesPlugProgramPda.deriveWhitelistedTokenPda(
feesPlugSolanaProgramId,
susdcMint_
);
// accounts 5 - receiver address
accounts[5] = userAddress_;
// accounts 6 - receiver ata address
accounts[6] = SolanaPDA.deriveTokenAtaAddress(userAddress_, susdcMint_);
// accounts 7 - vault ata address
accounts[7] = SolanaPDA.deriveTokenAtaAddress(vaultConfigPda, susdcMint_);
// accounts 8 - system program id
accounts[8] = SYSTEM_PROGRAM_ID;
// accounts 9 - token program id
accounts[9] = TOKEN_PROGRAM_ID;
// accounts 10 - associated token program id
accounts[10] = ASSOCIATED_TOKEN_PROGRAM_ID;

bytes1[] memory accountFlags = new bytes1[](11);
// tmpReturnStoragePda is writable
accountFlags[0] = bytes1(0x01); // true
// programConfigPda is not writable
accountFlags[1] = bytes1(0x00); // false
// vaultConfigPda is writable
accountFlags[2] = bytes1(0x01); // true
// token mint address is not writable
accountFlags[3] = bytes1(0x00); // false
// whitelistedTokenPda is not writable
accountFlags[4] = bytes1(0x00); // false
// receiver address is writable
accountFlags[5] = bytes1(0x01); // true
// receiver ata address is writable
accountFlags[6] = bytes1(0x01); // true
// vault ata address is writable
accountFlags[7] = bytes1(0x01); // true
// system program id is not writable
accountFlags[8] = bytes1(0x00); // false
// token program id is not writable
accountFlags[9] = bytes1(0x00); // false
// associated token program id is not writable
accountFlags[10] = bytes1(0x00); // false

// withdraw instruction discriminator
bytes8 instructionDiscriminator = 0xb712469c946da122;

bytes[] memory functionArguments = new bytes[](2);
bool isNative = false;
// here on purpose we do not convert to uint64 as feesPlug withdraw function expects uint256
uint64 withdrawAmountU64 = convertToSolanaUint64(withdrawAmount_);
functionArguments[0] = abi.encode(withdrawAmountU64);
functionArguments[1] = abi.encode(isNative ? 1 : 0);

string[] memory functionArgumentTypeNames = new string[](2);
functionArgumentTypeNames[0] = "u64"; // this should fit most of the cases (but our BorshEncoder supports max 128 bits)
functionArgumentTypeNames[1] = "u8"; // bool is encoded as 1 or 0

return
SolanaInstruction({
data: SolanaInstructionData({
programId: feesPlugSolanaProgramId,
instructionDiscriminator: instructionDiscriminator,
accounts: accounts,
functionArguments: functionArguments
}),
description: SolanaInstructionDataDescription({
accountFlags: accountFlags,
functionArgumentTypeNames: functionArgumentTypeNames
})
});
}
}

// convert EVM uint256 18 decimals to Solana uint64 6 decimals
function convertToSolanaUint64(uint256 amount) pure returns (uint64) {
uint256 scaledAmount = amount / 10 ** 12;
require(scaledAmount <= type(uint64).max, "Amount exceeds uint64 max");
return uint64(scaledAmount);
Comment on lines +451 to +455
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent loss when converting to Solana decimals

convertToSolanaUint64 floors values that are not multiples of 1e12, so withdrawing an amount like 1 wei burns the full credit but forwards 0 to Solana. This should revert on unsupported decimal precision so users never lose residual value. Add a remainder check before dividing.

 function convertToSolanaUint64(uint256 amount) pure returns (uint64) {
-    uint256 scaledAmount = amount / 10 ** 12;
+    if (amount % 10 ** 12 != 0) revert InvalidAmount();
+    uint256 scaledAmount = amount / 10 ** 12;
     require(scaledAmount <= type(uint64).max, "Amount exceeds uint64 max");
     return uint64(scaledAmount);
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/evmx/fees/Credit.sol around lines 456 to 460, the
convertToSolanaUint64 function currently floors values by dividing by 10**12
which can silently drop residual wei; add a remainder check before scaling so
the function reverts on unsupported precision. Specifically, require(amount %
(10**12) == 0, "Unsupported decimal precision") (use uint256 literal for the
modulus), then perform the division and keep the existing uint64 overflow
require; this prevents accidental loss of value by reverting when the input has
non-zero lower 12 decimals.

}
31 changes: 21 additions & 10 deletions contracts/evmx/fees/FeesManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
pragma solidity ^0.8.21;

import "./Credit.sol";
import {ForwarderSolana} from "../helpers/ForwarderSolana.sol";

/// @title FeesManager
/// @notice Contract for managing fees
Expand Down Expand Up @@ -50,7 +51,8 @@ contract FeesManager is Credit {
address feesPool_,
address owner_,
uint256 fees_,
bytes32 sbType_
bytes32 sbType_,
address forwarderSolana_
) public reinitializer(2) {
evmxSlug = evmxSlug_;
feesPool = IFeesPool(feesPool_);
Expand All @@ -59,6 +61,15 @@ contract FeesManager is Credit {

_initializeOwner(owner_);
_initializeAppGateway(addressResolver_);
forwarderSolana = ForwarderSolana(forwarderSolana_);
}

function setFeesPlugSolanaProgramId(bytes32 feesPlugSolanaProgramId_) external onlyOwner {
feesPlugSolanaProgramId = feesPlugSolanaProgramId_;
}

function setSusdcSolanaProgramId(bytes32 susdcSolanaProgramId_) external onlyOwner {
susdcSolanaProgramId = susdcSolanaProgramId_;
}

function setChainMaxFees(
Expand Down Expand Up @@ -111,25 +122,25 @@ contract FeesManager is Credit {
/// @param assignTo_ The address of the transmitter
function unblockAndAssignCredits(
bytes32 payloadId_,
address assignTo_
address assignTo_,
uint256 amount_
) external override onlyWatcher {
uint256 blockedCredits_ = blockedCredits[payloadId_];
if (blockedCredits_ == 0) return;

address consumeFrom = watcher__().getPayload(payloadId_).consumeFrom;
Payload memory payload = watcher__().getPayload(payloadId_);
address consumeFrom = payload.consumeFrom;

// Unblock credits from the original user
userBlockedCredits[consumeFrom] -= blockedCredits_;
userBlockedCredits[consumeFrom] -= amount_;
blockedCredits[payloadId_] -= amount_;

// Burn tokens from the original user
_burn(consumeFrom, blockedCredits_);

_burn(consumeFrom, amount_);
// Mint tokens to the transmitter
_mint(assignTo_, blockedCredits_);
_mint(assignTo_, amount_);

// Clean up storage
delete blockedCredits[payloadId_];
emit CreditsUnblockedAndAssigned(payloadId_, consumeFrom, assignTo_, blockedCredits_);
emit CreditsUnblockedAndAssigned(payloadId_, consumeFrom, assignTo_, amount_);
}

function unblockCredits(bytes32 payloadId_) external override onlyWatcher {
Expand Down
Loading