Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ 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
public let inVault: PublicKey
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,
Expand All @@ -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
}
Expand All @@ -68,7 +78,8 @@ extension CoinbaseStableSwapperProgram {
inVault: inVault.publicKey,
outVault: outVault.publicKey,
inVaultTokenAccount: inVaultTokenAccount.publicKey,
outVaultTokenAccount: outVaultTokenAccount.publicKey
outVaultTokenAccount: outVaultTokenAccount.publicKey,
whitelist: whitelist.publicKey
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {

Expand All @@ -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

Expand All @@ -59,6 +61,7 @@ extension CoinbaseStableSwapperProgram {
fromMint: PublicKey,
toMint: PublicKey,
user: PublicKey,
whitelist: PublicKey,
amountIn: UInt64,
minAmountOut: UInt64
) {
Expand All @@ -74,6 +77,7 @@ extension CoinbaseStableSwapperProgram {
self.fromMint = fromMint
self.toMint = toMint
self.user = user
self.whitelist = whitelist
self.amountIn = amountIn
self.minAmountOut = minAmountOut
}
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ extension SwapInstructionBuilder {
fromMint: fromMint.address,
toMint: toMint.address,
user: owner,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: amount
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ extension SwapInstructionBuilder {
fromMint: .usdc,
toMint: .usdf,
user: sender,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: amount
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ extension SwapInstructionBuilder {
fromMint: fromMintMetadata.address,
toMint: toMintMetadata.address,
user: swapAuthority,
whitelist: swapAccounts.whitelist,
amountIn: amount,
minAmountOut: minOutput
).instruction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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(
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions FlipcashTests/WalletConnectionPoolResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading