diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift index f5565b6bd..94029c6dd 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram+PDAs.swift @@ -38,14 +38,6 @@ 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 @@ -53,7 +45,6 @@ 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, @@ -67,8 +58,7 @@ 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 whitelist = deriveWhitelistAddress() + let outVaultTokenAccount = deriveVaultTokenAccountAddress(vault: outVault.publicKey) else { return nil } @@ -78,8 +68,7 @@ extension CoinbaseStableSwapperProgram { inVault: inVault.publicKey, outVault: outVault.publicKey, inVaultTokenAccount: inVaultTokenAccount.publicKey, - outVaultTokenAccount: outVaultTokenAccount.publicKey, - whitelist: whitelist.publicKey + outVaultTokenAccount: outVaultTokenAccount.publicKey ) } } diff --git a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift index 0a39ed526..1a52b18a1 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/Programs/CoinbaseStableSwapperProgram.PoolAccount.swift @@ -7,14 +7,16 @@ import Foundation extension CoinbaseStableSwapperProgram { - /// The on-chain liquidity pool account. + /// The on-chain liquidity pool account, as laid out after the role-based + /// authority migration (coinbase/stable-swapper#20). /// - /// Layout: `[8 discriminator][32 operations_authority][32 pause_authority][32 fee_recipient]...` + /// Layout: `[8 discriminator][32 pause_authority][32 unpause_authority] + /// [32 treasury_authority][32 configure_authority][32 fee_recipient]...` public struct PoolAccount: Equatable, Sendable { public let feeRecipient: PublicKey - private static let feeRecipientOffset = 8 + 32 + 32 + private static let feeRecipientOffset = 8 + 32 * 4 /// 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 b8591987c..63e5d8ef3 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 (16, in this exact order): + /// Account list (15, in this exact order): /// 0. [] pool /// 1. [] inVault /// 2. [] outVault @@ -22,10 +22,9 @@ extension CoinbaseStableSwapperProgram { /// 9. [] fromMint /// 10. [] toMint /// 11. [WRITE, SIGNER] user - /// 12. [] whitelist - /// 13. [] TokenProgram - /// 14. [] AssociatedTokenProgram - /// 15. [] SystemProgram + /// 12. [] TokenProgram + /// 13. [] AssociatedTokenProgram + /// 14. [] SystemProgram /// public struct Swap { @@ -41,7 +40,6 @@ 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 @@ -61,7 +59,6 @@ extension CoinbaseStableSwapperProgram { fromMint: PublicKey, toMint: PublicKey, user: PublicKey, - whitelist: PublicKey, amountIn: UInt64, minAmountOut: UInt64 ) { @@ -77,7 +74,6 @@ extension CoinbaseStableSwapperProgram { self.fromMint = fromMint self.toMint = toMint self.user = user - self.whitelist = whitelist self.amountIn = amountIn self.minAmountOut = minAmountOut } @@ -108,7 +104,6 @@ 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 c20c1bdee..f6b970f56 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+StatelessSwap.swift @@ -141,7 +141,6 @@ 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 820ccbcc9..20d1fa29e 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdcToUsdf.swift @@ -164,7 +164,6 @@ 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 11adfbcdc..046caf110 100644 --- a/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift +++ b/FlipcashCore/Sources/FlipcashCore/Solana/SwapInstructionBuilder+UsdfToUsdc.swift @@ -141,7 +141,6 @@ 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 9f0ca6c73..1a1af4def 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/CoinbaseStableSwapperPoolAccountTests.swift @@ -5,21 +5,34 @@ import Testing @Suite("CoinbaseStableSwapperProgram.PoolAccount") struct CoinbaseStableSwapperPoolAccountTests { - /// Minimum account length: 8 discriminator + 32 ops authority - /// + 32 pause authority + 32 fee recipient. - private static let minimumLength = 8 + 32 + 32 + 32 + /// 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/" 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)) // operations authority - data.append(Data(repeating: 0xCC, count: 32)) // pause authority + 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(feeRecipient)) data.append(Data(repeating: 0xDD, count: trailing)) return data } @Test( - "Parses the fee recipient at offset 72, with or without trailing fields", + "Parses the fee recipient at offset 136, with or without trailing fields", arguments: [0, 128] ) func initAccountData_validLayout_parsesFeeRecipient(trailing: Int) throws { @@ -32,6 +45,17 @@ 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 60f3acb51..3ff39ee6b 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, whitelist") + @Test("Coinbase swap accounts: pool PDAs, sender ATAs, fee recipient, programs") func coinbase_swapAccounts() throws { let instructions = Self.makeCoinbaseInstructions() let ix = instructions[6] @@ -272,7 +272,6 @@ 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( @@ -293,7 +292,10 @@ 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 == whitelist) + #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) } @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 33ecce970..26bee4b7a 100644 --- a/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift +++ b/FlipcashCore/Tests/FlipcashCoreTests/SwapInstructionBuilderUsdfToUsdcTests.swift @@ -248,11 +248,13 @@ struct SwapInstructionBuilderUsdfToUsdcTests { #expect(ix.accounts[11].isSigner == true) } - @Test("Swap whitelist (account 12) is the Coinbase whitelist PDA") - func swap_whitelist() { + @Test("Swap programs (accounts 12-14) follow the user directly; there is no whitelist account") + func swap_programs() { let ix = Self.makeInstructions()[7] - let expected = CoinbaseStableSwapperProgram.deriveWhitelistAddress()!.publicKey - #expect(ix.accounts[12].publicKey == expected) + #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) } // MARK: - Discriminator + data layout (24 bytes) diff --git a/FlipcashTests/WalletConnectionPoolResolutionTests.swift b/FlipcashTests/WalletConnectionPoolResolutionTests.swift index b646f29ea..88befc78f 100644 --- a/FlipcashTests/WalletConnectionPoolResolutionTests.swift +++ b/FlipcashTests/WalletConnectionPoolResolutionTests.swift @@ -13,7 +13,8 @@ import FlipcashCore struct WalletConnectionPoolResolutionTests { private nonisolated static func poolAccountData(feeRecipient: [UInt8]) -> Data { - var data = Data(repeating: 0, count: 8 + 32 + 32) + // 8 discriminator + pause, unpause, treasury, configure authorities. + var data = Data(repeating: 0, count: 8 + 32 * 4) data.append(Data(feeRecipient)) return data }