diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift index 94029c6dd..f5565b6bd 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift @@ -38,6 +38,14 @@ extension CoinbaseStableSwapperProgram { ) } + /// PDA: ["address_whitelist"] @ CoinbaseStableSwapperProgram + public static func deriveWhitelistAddress() -> ProgramDerivedAccount? { + ProgramDerivedAccount.findProgramAddress( + seeds: [Data("address_whitelist".utf8)], + program: address + ) + } + /// The pool-side accounts a `Swap` instruction targets for a mint pair. public struct SwapAccounts: Equatable, Sendable { public let pool: PublicKey @@ -45,6 +53,7 @@ extension CoinbaseStableSwapperProgram { public let outVault: PublicKey public let inVaultTokenAccount: PublicKey public let outVaultTokenAccount: PublicKey + public let whitelist: PublicKey } /// Derives every pool-side account `Swap` needs for the given mint pair, @@ -58,7 +67,8 @@ extension CoinbaseStableSwapperProgram { let inVault = deriveTokenVaultAddress(pool: pool.publicKey, mint: fromMint), let outVault = deriveTokenVaultAddress(pool: pool.publicKey, mint: toMint), let inVaultTokenAccount = deriveVaultTokenAccountAddress(vault: inVault.publicKey), - let outVaultTokenAccount = deriveVaultTokenAccountAddress(vault: outVault.publicKey) + let outVaultTokenAccount = deriveVaultTokenAccountAddress(vault: outVault.publicKey), + let whitelist = deriveWhitelistAddress() else { return nil } @@ -68,7 +78,8 @@ extension CoinbaseStableSwapperProgram { inVault: inVault.publicKey, outVault: outVault.publicKey, inVaultTokenAccount: inVaultTokenAccount.publicKey, - outVaultTokenAccount: outVaultTokenAccount.publicKey + outVaultTokenAccount: outVaultTokenAccount.publicKey, + whitelist: whitelist.publicKey ) } } diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift index 1a52b18a1..0a39ed526 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift @@ -7,16 +7,14 @@ import Foundation extension CoinbaseStableSwapperProgram { - /// The on-chain liquidity pool account, as laid out after the role-based - /// authority migration (coinbase/stable-swapper#20). + /// The on-chain liquidity pool account. /// - /// Layout: `[8 discriminator][32 pause_authority][32 unpause_authority] - /// [32 treasury_authority][32 configure_authority][32 fee_recipient]...` + /// Layout: `[8 discriminator][32 operations_authority][32 pause_authority][32 fee_recipient]...` public struct PoolAccount: Equatable, Sendable { public let feeRecipient: PublicKey - private static let feeRecipientOffset = 8 + 32 * 4 + private static let feeRecipientOffset = 8 + 32 + 32 /// Parses the raw pool account data, returning `nil` when the data is /// too short or the fee recipient bytes are not a valid public key. diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift index 63e5d8ef3..b8591987c 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.Swap.swift @@ -9,7 +9,7 @@ extension CoinbaseStableSwapperProgram { /// Anchor-format instruction for the Coinbase Stable Swapper's `swap` handler. /// - /// Account list (15, in this exact order): + /// Account list (16, in this exact order): /// 0. [] pool /// 1. [] inVault /// 2. [] outVault @@ -22,9 +22,10 @@ extension CoinbaseStableSwapperProgram { /// 9. [] fromMint /// 10. [] toMint /// 11. [WRITE, SIGNER] user - /// 12. [] TokenProgram - /// 13. [] AssociatedTokenProgram - /// 14. [] SystemProgram + /// 12. [] whitelist + /// 13. [] TokenProgram + /// 14. [] AssociatedTokenProgram + /// 15. [] SystemProgram /// public struct Swap { @@ -40,6 +41,7 @@ extension CoinbaseStableSwapperProgram { public let fromMint: PublicKey public let toMint: PublicKey public let user: PublicKey + public let whitelist: PublicKey public let amountIn: UInt64 public let minAmountOut: UInt64 @@ -59,6 +61,7 @@ extension CoinbaseStableSwapperProgram { fromMint: PublicKey, toMint: PublicKey, user: PublicKey, + whitelist: PublicKey, amountIn: UInt64, minAmountOut: UInt64 ) { @@ -74,6 +77,7 @@ extension CoinbaseStableSwapperProgram { self.fromMint = fromMint self.toMint = toMint self.user = user + self.whitelist = whitelist self.amountIn = amountIn self.minAmountOut = minAmountOut } @@ -104,6 +108,7 @@ extension CoinbaseStableSwapperProgram.Swap: InstructionType { .readonly(publicKey: fromMint), .readonly(publicKey: toMint), .writable(publicKey: user, signer: true), + .readonly(publicKey: whitelist), .readonly(publicKey: TokenProgram.address), .readonly(publicKey: AssociatedTokenProgram.address), .readonly(publicKey: SystemProgram.address), diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift index f6b970f56..c20c1bdee 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift @@ -141,6 +141,7 @@ extension SwapInstructionBuilder { fromMint: fromMint.address, toMint: toMint.address, user: owner, + whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: amount ).instruction() diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift index 20d1fa29e..820ccbcc9 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift @@ -164,6 +164,7 @@ extension SwapInstructionBuilder { fromMint: .usdc, toMint: .usdf, user: sender, + whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: amount ).instruction() diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift index 046caf110..11adfbcdc 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift @@ -141,6 +141,7 @@ extension SwapInstructionBuilder { fromMint: fromMintMetadata.address, toMint: toMintMetadata.address, user: swapAuthority, + whitelist: swapAccounts.whitelist, amountIn: amount, minAmountOut: minOutput ).instruction() diff --git a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift index 1a1af4def..9f0ca6c73 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift @@ -5,34 +5,21 @@ import Testing @Suite("CoinbaseStableSwapperProgram.PoolAccount") struct CoinbaseStableSwapperPoolAccountTests { - /// Minimum account length: 8 discriminator + 32 pause authority - /// + 32 unpause authority + 32 treasury authority + 32 configure authority - /// + 32 fee recipient. - private static let minimumLength = 8 + 32 * 5 - - /// First 315 bytes of mainnet pool CrDL9SoCyW1tBgn8k7rgGSpWhnszneWDbvKvqPAU4PL9 - /// after the 2026-09-08 MigrateAuthorities instruction. The live account is - /// 2107 bytes with the remainder zeroed. - private static let mainnetPoolPrefix = - "QiYRQLxQRIEFHqE9vluQFKO1wbEwnd22aRe9qGrV03SIsz1AV7GqT/yEzcR/f+ALaKG4KMxbBfZ5dTNFPqxxZHMfbTqNfj6St0p+" + - "+yObz2IILMcKsoko07O+oRSDek7YwzrH9TroL1+QbHdT/t9qpcq4Kyx8OPWZm79AIUM9UlN+X6ujF0hPgzT4z8SXtrVTfBhZj7Lz" + - "SPTgCpHoi6cjfPqXvflOJvRBAgAAAN0H70q0C5DeChX575Umuo4KwnYx+lqZmPTGnq1wMK2QSoyv1lJlvQkMgeq0VkN3NML2MHaz" + - "cTzSusODf5c6RhACAAAAxvp6877brTo9ZfNqq8l0MbG75MLS9uDkfKYCA0UvXWE908SAij1Ps5+uycukm1pjSlLt4RVI9SSqIPLN" + - "Ru+AcQAAAAAAAAAAAAD/" + /// Minimum account length: 8 discriminator + 32 ops authority + /// + 32 pause authority + 32 fee recipient. + private static let minimumLength = 8 + 32 + 32 + 32 private static func accountData(feeRecipient: [UInt8], trailing: Int = 0) -> Data { var data = Data(repeating: 0xAA, count: 8) // discriminator - data.append(Data(repeating: 0xBB, count: 32)) // pause authority - data.append(Data(repeating: 0xCC, count: 32)) // unpause authority - data.append(Data(repeating: 0xEE, count: 32)) // treasury authority - data.append(Data(repeating: 0x11, count: 32)) // configure authority + data.append(Data(repeating: 0xBB, count: 32)) // operations authority + data.append(Data(repeating: 0xCC, count: 32)) // pause authority data.append(Data(feeRecipient)) data.append(Data(repeating: 0xDD, count: trailing)) return data } @Test( - "Parses the fee recipient at offset 136, with or without trailing fields", + "Parses the fee recipient at offset 72, with or without trailing fields", arguments: [0, 128] ) func initAccountData_validLayout_parsesFeeRecipient(trailing: Int) throws { @@ -45,17 +32,6 @@ struct CoinbaseStableSwapperPoolAccountTests { #expect(account.feeRecipient == (try PublicKey(feeRecipientBytes))) } - @Test("Parses the fee recipient from the migrated mainnet pool account") - func initAccountData_mainnetPool_parsesFeeRecipient() throws { - let prefix = try #require(Data(base64Encoded: Self.mainnetPoolPrefix)) - #expect(prefix.count == 315) - var data = prefix - data.append(Data(repeating: 0, count: 2107 - prefix.count)) - - let account = try #require(CoinbaseStableSwapperProgram.PoolAccount(accountData: data)) - #expect(account.feeRecipient == (try PublicKey(base58: "4ZnFXk7KyB5khDqjWSHqHBQH1nQCnmvkr1pRFivWcP7e"))) - } - @Test("Rejects account data shorter than the fee recipient bounds") func initAccountData_shortData_returnsNil() { let short = Data(repeating: 0xAA, count: Self.minimumLength - 1) diff --git a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift index 3ff39ee6b..60f3acb51 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdcToUsdfTests.swift @@ -262,7 +262,7 @@ struct SwapInstructionBuilderUsdcToUsdfTests { #expect(coinbase.data == legacy.data) } - @Test("Coinbase swap accounts: pool PDAs, sender ATAs, fee recipient, programs") + @Test("Coinbase swap accounts: pool PDAs, sender ATAs, fee recipient, whitelist") func coinbase_swapAccounts() throws { let instructions = Self.makeCoinbaseInstructions() let ix = instructions[6] @@ -272,6 +272,7 @@ struct SwapInstructionBuilderUsdcToUsdfTests { let outVault = try #require(CoinbaseStableSwapperProgram.deriveTokenVaultAddress(pool: pool, mint: .usdf)).publicKey let inVaultTokenAccount = try #require(CoinbaseStableSwapperProgram.deriveVaultTokenAccountAddress(vault: inVault)).publicKey let outVaultTokenAccount = try #require(CoinbaseStableSwapperProgram.deriveVaultTokenAccountAddress(vault: outVault)).publicKey + let whitelist = try #require(CoinbaseStableSwapperProgram.deriveWhitelistAddress()).publicKey let senderUsdcAta = instructions[4].accounts[1].publicKey let senderUsdfAta = instructions[2].accounts[1].publicKey let feeRecipientUsdcAta = try #require( @@ -292,10 +293,7 @@ struct SwapInstructionBuilderUsdcToUsdfTests { #expect(ix.accounts[10].publicKey == PublicKey.usdf) #expect(ix.accounts[11].publicKey == Self.sender) #expect(ix.accounts[11].isSigner) - #expect(ix.accounts[12].publicKey == TokenProgram.address) - #expect(ix.accounts[13].publicKey == AssociatedTokenProgram.address) - #expect(ix.accounts[14].publicKey == SystemProgram.address) - #expect(ix.accounts.count == 15) + #expect(ix.accounts[12].publicKey == whitelist) } @Test("Coinbase swap data: discriminator + amountIn(8 LE) + minAmountOut(8 LE), both equal to amount") diff --git a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift index 26bee4b7a..33ecce970 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift @@ -248,13 +248,11 @@ struct SwapInstructionBuilderUsdfToUsdcTests { #expect(ix.accounts[11].isSigner == true) } - @Test("Swap programs (accounts 12-14) follow the user directly; there is no whitelist account") - func swap_programs() { + @Test("Swap whitelist (account 12) is the Coinbase whitelist PDA") + func swap_whitelist() { let ix = Self.makeInstructions()[7] - #expect(ix.accounts[12].publicKey == TokenProgram.address) - #expect(ix.accounts[13].publicKey == AssociatedTokenProgram.address) - #expect(ix.accounts[14].publicKey == SystemProgram.address) - #expect(ix.accounts.count == 15) + let expected = CoinbaseStableSwapperProgram.deriveWhitelistAddress()!.publicKey + #expect(ix.accounts[12].publicKey == expected) } // MARK: - Discriminator + data layout (24 bytes) diff --git a/FlipcashTests/WalletConnectionPoolResolutionTests.swift b/FlipcashTests/WalletConnectionPoolResolutionTests.swift index 88befc78f..b646f29ea 100644 --- a/FlipcashTests/WalletConnectionPoolResolutionTests.swift +++ b/FlipcashTests/WalletConnectionPoolResolutionTests.swift @@ -13,8 +13,7 @@ import FlipcashCore struct WalletConnectionPoolResolutionTests { private nonisolated static func poolAccountData(feeRecipient: [UInt8]) -> Data { - // 8 discriminator + pause, unpause, treasury, configure authorities. - var data = Data(repeating: 0, count: 8 + 32 * 4) + var data = Data(repeating: 0, count: 8 + 32 + 32) data.append(Data(feeRecipient)) return data }