> For the complete documentation index, see [llms.txt](https://docs.rome.builders/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rome.builders/ko/developer-guides/integrate-solana-program.md).

# Solana 프로그램 통합

Solana 프로그램을 Rome EVM과 통합하세요 — Solana 서명자가 누구인지, 가치가 어떻게 프로그램에 도달하고 다시 돌아오는지, 단일 경로 규칙, 원자적 vs 반복적, 그리고 계정 프로비저닝.

이 가이드는 Rome EVM 사용자가 여러분의 프로그램을 사용하도록 하려는 Solana 프로그램 팀을 위한 것입니다 — EVM 계정이 가치를 보관해야 하는 프라이버시, 대출, 또는 볼트 프로그램입니다. 이는 다음을 바탕으로 합니다 [EVM에서 Solana 호출](/ko/developer-guides/call-solana-from-evm.md) (프리컴파일 메커니즘)을 바탕으로 하며, 실제 프로그램이 다른 쪽에 있을 때 떠오르는 질문에 답합니다: *누가* 서명하고, *어떻게* 토큰이 들어오고 나가는지, 그리고 *어떤* 제한이 설계를 규정하는지입니다.

이 가이드 전반에서 사용하는 흐름은 다음과 같습니다: EVM 계정이 USDC를 여러분의 프로그램으로 옮기고("shield" 또는 "deposit"), 여러분의 프로그램이 그것을 해당 사용자 대신 보관한 뒤, 나중에 다시 돌려줍니다("unshield" 또는 "withdraw"). 동사는 여러분의 것에 맞게 바꾸세요.

## 1. 모든 EVM 주소에는 하나의 Solana 신원이 있다

Rome은 모든 EVM 주소 — 사용자와 계약 모두 — 에 대해 하나의 Solana 계정을 파생하며, 이를 **외부 권한**:

```
external_auth(addr) = find_program_address(["EXTERNAL_AUTHORITY", addr_h160], rome_evm_program_id)
```

가 소유합니다. 그곳이 주소의 연관 토큰 계정이 있는 곳입니다(즉, `wUSDC.balanceOf(addr)` 가 읽어오는 곳)이며, 그 주소를 대신해 만들어진 CPI에 서명하는 키이기도 합니다. 여러분의 프로그램은 32바이트 Solana pubkey만 보며, H160은 절대 보지 못합니다.

**누가 서명하는지는 누가 프리컴파일을 호출하느냐에 따라 결정됩니다:**

| 프리컴파일은 다음에 의해 호출됩니다                  | Solana 서명자는                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 하나의 **계약**, 일반적인 `CALL`              | 위의 **계약 자신의** 외부 권한(`external_auth(contract)`), 그리고 요청한 계약의 salted sub-PDA도 포함됩니다                                                                                                                                                                                                                                                                                                                                                                          |
| 하나의 **사용자**, 프리컴파일 주소로 직접 보내진 트랜잭션에서 | 위의 **사용자 자신의** 외부 권한                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 계약이 `DELEGATECALL`                   | 벌거벗은 권한은 거부됩니다(`DelegatecallOwnerAuthority`). `invoke` 서명자 없이, 그리고 `invoke_signed` salt가 붙은 sub-PDA는 계속 동작하지만 — 그 sub-PDA들은 실행 중인 계약의 주소로 이름공간이 나뉩니다(`["EXTERNAL_AUTHORITY", caller, running_contract, salt]`), 그래서 `pda_with_salt` 은 그것들을 파생하지 않습니다. HelperProgram 중에서는 생성과 자금 조달 셀렉터만 통과합니다(`create_ata*`, `create_pda*`, `swap_gas_to_lamports`, `init_spl_mint`, `create_and_init_mint`); 이는 그 대상이 호출자에 묶여 있거나 운영자가 자금을 대기 때문이지, 아무것도 서명하지 않기 때문이 아닙니다 |

그 결과는 여러분이 이미 알고 있는 ERC-20 규칙입니다: **계약은 혼자서 사용자의 자산을 움직일 수 없습니다.** 계약은 자기 자신의 신원처럼 동작하고, 사용자가 먼저 권한을 부여합니다. Solana 쪽에서 움직여야 하는 토큰의 경우, 그 권한 부여는 사용자의 토큰 계정에 대한 SPL delegation입니다; 래퍼의 ERC-20 `approve` 은 래퍼 자신의 전송이 존중하는 EVM 쪽 허용량일 뿐입니다. 그것이 Rome의 토큰 래퍼가 동작하는 방식이고, 여러분의 어댑터도 그렇게 동작합니다(3절).

Salt가 붙은 sub-PDA: `invoke_signed` 다음의 목록을 받습니다 `bytes32` salt이며, Rome은 다음으로 서명합니다 `["EXTERNAL_AUTHORITY", caller_h160, salt]` 각각에 대해 (bump가 덧붙습니다; 직접 호출에서는 호출자가 계약입니다). `HelperProgram.pda_with_salt(addr, salt)` 은 Solidity에서 해당 주소를 반환합니다. 여러분의 프로그램이 각 포지션마다 별도의 소유 계정을 원한다면 사용자마다 하나의 salt(예: 사용자의 주소)를 사용하세요.

자금 조달: 외부 권한 PDA는 ATA를 소유하기 위해 존재할 필요가 없습니다(`create_ata` 는 idempotent이고 트랜잭션의 Solana fee payer가 rent를 부담합니다). 이 계정이 여러분의 instruction이 만드는 계정에 자금을 댈 때만 존재하고 lamports를 보유하면 됩니다. `HelperProgram.create_pda(addr)` 는 이를 단순한 System `CreateAccount`으로 생성하므로 한 번만 호출하세요; idempotent가 아닙니다. `swap_gas_to_lamports(n)` 은 해당 주소의 PDA에 `n` lamports를 트랜잭션의 Solana fee payer로부터 적립하고, 호출자는 Rome 가스로 그것들을 지불합니다: legacy-track CPI가 fee payer 잔액에 미치는 모든 효과는 트랜잭션 수수료에 더해지고 가스 가격으로 사용자에게 청구되며, CPI가 생성한 계정의 rent가 청구되는 방식과 같습니다. Solana lane의 프로비저닝 단계는 [Solana에서 EVM 호출](/ko/developer-guides/call-evm-from-solana.md)에서 설명합니다; 계약은 자신의 생성자 또는 첫 호출에서 자기 PDA를 프로비저닝합니다.

## 2. 사용자의 포지션을 보관하는 세 가지 방법

**A. 어댑터 계약(재사용 가능한 어댑터에 권장).** 하나의 Solidity 계약이 여러분의 프로그램 앞단에 놓입니다. 사용자는 어댑터의 권한에 자신의 토큰 계정에 대한 SPL delegation을 부여합니다; 어댑터는 토큰을 자신이 소유한 토큰 계정으로 끌어온 뒤, 자기 자신으로서 여러분의 프로그램을 호출하고, instruction data로 전달한 식별자 또는 사용자별 sub-PDA로 프로그램 내부에서 사용자의 포지션을 식별합니다. 반환할 때는 사용자의 자체 토큰 계정으로 돌려주라고 여러분의 프로그램에 지시합니다. 어댑터는 여러분의 프로그램이 알아야 할 유일한 Solana 신원입니다. 이 권한 부여는 SPL delegation이지, ERC-20 `approve`이 아닙니다; 3절에서 그 이유와 사용자 비용을 설명합니다.

**B. Solana 서명자로서의 사용자.** 프런트엔드는 사용자의 EVM 계정에서 시작된 트랜잭션을 **CpiProgram 프리컴파일로 직접 보냅니다** (`to = 0xff…08`, calldata = ABI 인코딩된 `invoke` 또는 `invoke_signed`), 그래서 사용자의 자체 외부 권한이 서명하고 여러분의 프로그램은 그것으로 포지션을 식별합니다. 어댑터 계약도 없고 delegation도 없지만, 각 작업은 지갑에서 보이는 불투명한 calldata 트랜잭션이며, CPI 외에 흐름에 필요한 모든 것(ATA, 사전 전송)은 별도의 트랜잭션입니다. 이는 일회성 허가(사용자의 `approve_spl` 가 래퍼에 주는 허가)와 단순한 프로그램에는 적합하지만, 다단계 흐름에는 적합하지 않습니다. [Cardo](https://github.com/rome-protocol/cardo) 는 이 형식을 사용합니다: 프런트엔드가 `invoke` 를 사용자의 지갑에서 프리컴파일로 바로 보냅니다.

**C. 수탁 없는 라우터.** 사용자의 토큰을 절대 보유하지 않는 계약입니다. 사용자는 라우터의 권한에 자기 자신의 토큰 계정에 대한 SPL delegation을 부여합니다; 라우터는 그 권한을 전송 권한으로 하여 여러분의 프로그램에 CPI를 실행하므로, 토큰은 사용자의 토큰 계정에서 여러분의 프로그램 계정으로(또는 다시 사용자에게) 이동하고, 라우터는 호출 사이에 아무것도 보유하지 않습니다. 라우터는 모든 사용자 측 계정을 `msg.sender`에서 파생하므로 한 사용자의 허가가 다른 사용자의 계정을 가리킬 수 없고, 일반적인 `invoke` 패스스루처럼 동작하는 대신 고정된 instruction shape를 가진 하나의 고정 프로그램을 호출합니다.  `RomeDexRouter` 에서 [rome-dex](https://github.com/rome-protocol/rome-dex) 는 이런 방식으로 작성되어 있습니다; rome-dex의 EVM lane도 원시 직접 `invoke` 형태 B를 제공합니다. 이것은 A와 같은 허가가 필요하고, 같은 one-delegate 비용(3절)을 가지며, pull 단계는 없습니다.

이 가이드의 나머지는 A를 따릅니다; C는 같은 허가와 같은 서명 규칙을 사용합니다.

## 3. 토큰의 입출금

Rome의 USDC는 하나의 SPL 민트를 양쪽에서 본 것입니다:  `wUSDC` EVM 측의 ERC-20 래퍼와 Solana 측 외부 권한의 ATA입니다. 사용자의 자체 주소에 대해서는 둘이 같은 잔액입니다: `wUSDC.balanceOf(user)` 는 사용자의 ATA를 읽습니다. 중간에 브리지나 래핑 단계는 없습니다.

**계약의 래퍼 잔액은 계약이 제어하는 토큰 계정이 아닙니다.** 팩토리는 `SPL_ERC20_cached` 일반적인 민트를 위해 배포하며, Hadrian의 `wUSDC` 도 그중 하나입니다. 그 래퍼는 계약을 대신해 서명할 수 없으므로, `wUSDC.transferFrom(user, adapter, amount)` 또는 `전송에서는 후크가 작동하지 않습니다` 가 토큰을 계약으로 보낼 때 SPL 토큰은 **래퍼 자신의 에스크로 토큰 계정** (`escrow_ata()`)으로 이동하고, 래퍼는 저장소에 계약별 금액을 기록합니다. `wUSDC.balanceOf(adapter)` 는 토큰 계정의 잔액이 아니라 에스크로 원장인 그 기록을 반환합니다. 이것은 `external_auth(adapter)`가 소유한 토큰 계정의 잔액이 아닙니다. 여러분의 어댑터는 CPI에서 그 토큰을 여러분의 프로그램에 넘길 수 없습니다; 그것들은 다시 래퍼를 통해서만 나갈 수 있습니다. 따라서 `transferFrom` 로 자금을 조달하는 어댑터는 Solana 쪽에서 쓸 수 있는 것이 없습니다.

작동하는 경로는 SPL delegation입니다:

* **허가.** 사용자는 `approve_spl(adapter, amount, mint)` 을 트랜잭션으로 **HelperProgram으로 직접 보냅니다** (`to = 0xff…09`), 여기서 `mint` 는 래퍼의 `mint_id()`입니다. 사용자의 자체 권한이 서명하고, 사용자의 토큰 계정에 설정되는 delegate는 어댑터의 외부 권한입니다.
* **들어갑니다.** 어댑터의 `deposit(amount)` 는 delegate 오버로드로 끌어옵니다 `HelperProgram.transfer_spl(srcAta, dstAta, amount, mint)`, 소스로는 사용자의 ATA를, 목적지로는 어댑터 자신의 ATA(`HelperProgram.ata(address(this), mint)`, 한 번 생성 `create_ata`)을 사용합니다. 어댑터의 권한이 delegate로서 서명합니다. 토큰은 이제 `external_auth(adapter)`가 소유한 토큰 계정에 있으며, 이것이 여러분의 CPI가 이름을 붙이는 계정입니다; 서명자와 소스는 어댑터 자신의 것이므로 추가 권한은 필요 없습니다. 이것들은 `wUSDC.balanceOf(adapter)`.
* **나타나지 않습니다.** 출력. **여러분의 프로그램의 withdraw instruction은** 사용자의 `ATA로 전송합니다,`HelperProgram.ata(user, mint) `wUSDC.balanceOf(user)`. 받는 쪽에는 사용자의 서명이 필요 없고, 잔액은 곧바로 `HelperProgram.create_ata(user, mint)` 로 첫 생성할 수 있습니다(idempotent).

**토큰 계정에는 delegate가 하나만 있습니다.** 위의 허가는 해당 계정에 대한 래퍼 자신의 허가를 대체합니다. 사용자가 래퍼에게 다시 허가를 줄 때까지, 그 주소에서 나가는 모든 `wUSDC` `전송에서는 후크가 작동하지 않습니다` 또는 `transferFrom` 전송은 SPL 오류로 revert됩니다 `0x4`. 이 허가를 사용하는 어떤 흐름이 끝난 뒤에는 `wUSDC.isEnabled(user)`를 호출하세요; 그것이 `false`를 반환하면 사용자에게 다음을 다시 보내라고 요청하세요 `approve_spl(wrapper, 2^64-1, mint)` 를 HelperProgram으로 다시 보내도록 하세요. 이것은 래퍼가 전제로 삼는 동일한 최대 허가입니다(참조 [한 번만: 래퍼에 허가 부여](/ko/developer-guides/dual-lane-app.md#one-time-grant-the-wrapper)). 상한이 있는 허가는 잔액이 줄어들면 사용자의 일반적인 `wUSDC` 전송을 다시 깨뜨릴 것이고, `isEnabled` 는 `true` 로 유지되므로 앱은 다시 요청해야 하는지 알 수 없습니다.

어느 래퍼가 민트를 가지는지 확인하려면 팩토리의 `wrapper_kind_by_mint(mint)` (팩토리 주소는 체인의 `contracts.json` 안에 [레지스트리](https://github.com/rome-protocol/rome-registry)). `1` 는 이 절에서 설명하는 캐시된 래퍼입니다. `2` 는 활성화된 transfer hook이 있는 Token-2022 민트를 위해 팩토리가 배포하는 직접-CPI 래퍼입니다; 이 가이드는 그것을 다루지 않습니다.  `tokens.json` 레지스트리의

## 4. 계약당 하나의 트랙, 그리고 트랜잭션당 하나의 트랙

Rome에는 Solana 상태를 바꾸는 두 계열의 프리컴파일이 있으며, **하나의 트랜잭션은 그중 정확히 하나만 사용합니다** — 런타임은 두 계열에서 상태를 바꾸는 호출을 섞은 트랜잭션을, 계약이 다르더라도 거부합니다.

| 트랙      | 프리컴파일                                                                                               | 효과가 반영되는 시점                                                 | 반복 실행에서 동작하는가                                               |
| ------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------- |
| **레거시** | `CpiProgram` `0xff…08` (`invoke`, `invoke_signed`), `HelperProgram` `0xff…09`, `출금` `0x42…16`       | 즉시, EVM 단계 내부의 실제 CPI로서                                     | **아니오** — 반복적 트랜잭션에서의 레거시 CPI는 `CpiProhibitedInIterativeTx` |
| **캐시됨** | `SplCached` `0xff…05`, `ASplCached` `0xff…06`, `SystemCached` `0xff…04`, `WithdrawCached` `0xff…0b` | 오버레이에 단계적으로 쌓인 뒤 트랜잭션 끝에서 CPI로 커밋됩니다; EVM revert는 그것들을 버립니다 | 예                                                           |

이것이 프로그램 통합에 의미하는 바:

* **자신의 프로그램 instruction을 호출하는 것은 레거시 트랙에서만 가능합니다.** 캐시된 트랙에는 일반적인 `invoke`이 없고; SPL 전송, ATA 생성, system-program 작업, 가스 인출을 포함합니다.
* 따라서 어댑터에 자금을 대는 단계도 CPI와 같은 트랜잭션에서 실행된다면 레거시 트랙이어야 합니다. 캐시된 래퍼의 `transferFrom` 는 캐시된 트랙 호출이므로 여러분의 CPI와 트랜잭션을 공유할 수 없습니다(그리고 3절에서 보듯, 그것은 에스크로 원장에만 적립될 뿐입니다). 대신 다음으로 끌어오세요 `HelperProgram.transfer_spl(userAta, adapterAta, amount, mint)` 을 사용하세요. 사용자가 어댑터를 SPL delegate로 만든 뒤(`approve_spl`, 사용자가 HelperProgram으로 직접 보냄). 끌어오기와 CPI는 둘 다 레거시 트랙이므로 하나의 트랜잭션에서 실행됩니다.
* **그 트랜잭션에서 래퍼는 제외하세요.** 래퍼의 변경 함수(`wUSDC.transfer`, `transferFrom`)을 어댑터에서 레거시 CPI와 같은 트랜잭션 안에서 호출하지 마세요. 다음을 확인하세요 `isEnabled(user)` 와 잔액은 프런트엔드에서 `eth_call`.
* **로 확인하세요.** 그리고 CPI 트랜잭션은 반드시 원자적이어야 합니다.

그것이 설계가 놓여 있는 한계입니다. `account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive` 및 `HelperProgram.user_balance` / `allowance_of` 는 미리 로드된 계정의 읽기입니다(CPI 없음). 어떤 트랙과 실행 모드에서도 허용됩니다. 사전 검사를 여기에 두세요. 한 가지 순서 규칙이 있습니다: 트랜잭션에서 캐시된 변경이 한 번 발생하면 그 뒤로는 레거시 트랙 읽기가 차단됩니다 — 먼저 읽고, 그다음 변경하세요.

## 5. 원자성이 예산이다

원자적 Rome 트랜잭션은 하나의 Solana 트랜잭션입니다: 대략 **140만 compute unit**,  **64개 주소** 의 계정 목록과 **4 KB** 의 wire size(Solana v1 트랜잭션)입니다. Rome 프로그램과 여러분의 CPI가 그 예산을 공유합니다. 래퍼 전송은 Hadrian에서 약 170K CU를 사용하고 여러분의 instruction은 자체 비용을 더합니다; Solana receipt의 `computeUnitsConsumed`, EVM이 아니라 `gasUsed`.

에 따라 측정하세요. 에뮬레이션이 트랜잭션이 맞지 않는다고 판단하면, Rome은 그것을 **반복적으로** 여러 Solana 트랜잭션에 걸쳐 실행하고, 그 안의 레거시 CPI는 실패합니다. deposit과 withdraw는 각각 하나의 토큰 이동과 여러분의 프로그램으로의 하나의 CPI로 유지하고, 그 주변의 Solidity는 작게 유지하세요. 그보다 큰 것은 둘의 사용자 트랜잭션으로 나누세요.

CPI 깊이: Solana는 4단계를 허용하고 Rome은 한 단계를 사용하므로, 여러분의 instruction은 2단계에서 실행되며 그 아래에 두 단계가 있습니다(여러분의 프로그램 → SPL Token이면 하나가 남습니다).

## 6. 계정 프로비저닝

여러분의 프로그램은 실행 시 Solidity에서 계정을 받지 않습니다. 프록시는 **에뮬레이션합니다** 전송하기 전에 모든 트랜잭션을, CPI가 건드린 모든 Solana 계정을 기록하고, 그것들을 Solana 트랜잭션에 넣습니다. 여러분의 어댑터는 `invoke_signed` 계정 목록에서 계정들을 이름 짓습니다; 에뮬레이션이 그것들이 해석됨을 증명합니다. 여기서 세 가지가 따릅니다:

* 여러분의 instruction이 건드리는 모든 계정은 어댑터가 가진 입력(자기 자신의 PDA와 sub-PDA, 여러분의 program ID, 민트들, 사용자의 주소)에서 파생 가능해야 합니다. `SystemProgram.find_program_address` 및 `CpiProgram.pdas_batch_derive` 는 Solidity에서 파생을 수행합니다.
* 아직 존재하지 않는 계정(새 포지션, 새 ATA)도 여전히 계정 목록에 이름이 있어야 합니다; 그 주소가 Solana 트랜잭션에 전달되는 것입니다.
* 트랜잭션이 건드리는 모든 것 — 사용자, 어댑터, 래퍼에 대한 Rome 자신의 계정까지 포함해서 — 는 64개 주소 아래에 있어야 합니다. 한 번의 호출로 10개 시장을 건드리는 설계는 맞지 않습니다.

Solana-wallet lane의 경우, `rome_emulateCallAccounts` 는 같은 발견 정보를 클라이언트에 노출하며; SDK가 그것을 사용합니다.

CPI가 절대 이름을 붙일 수 없는 두 계정: 트랜잭션을 제출하는 Solana fee payer(단순 System transfer instruction 내부를 제외하고)와 Rome 프로그램 자체입니다. Rome 프로그램으로 되돌아가는 CPI는 거부됩니다.

## 7. Solidity로 쓰는 deposit과 withdraw

레거시 트랙의 어댑터에 대한 최소 스케치입니다. 사용자는 이미 `approve_spl(adapter, amount, mint)` 를 HelperProgram으로 보냈습니다(3절). `PRIVACY_PROGRAM` 은 여러분의 program ID입니다; `encodeDeposit` / `encodeWithdraw` 는 여러분의 instruction data를 만듭니다(Borsh, little-endian); account meta는 instruction의 레이아웃을 따릅니다.

```solidity
import {ICrossProgramInvocation, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

contract PrivacyAdapter {
    ICrossProgramInvocation constant Cpi    = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
    IHelperProgram          constant Helper = IHelperProgram(0xFF00000000000000000000000000000000000009);
    bytes32 constant SPL_TOKEN = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; // TokenkegQ...

    bytes32 immutable PRIVACY_PROGRAM;
    bytes32 immutable USDC_MINT;

    constructor(bytes32 program, bytes32 mint) {
        PRIVACY_PROGRAM = program; USDC_MINT = mint;
        Helper.create_pda(address(this));                 // 어댑터 자신의 Solana 신원
        Helper.create_ata(address(this), USDC_MINT);      // 그리고 그것이 소유하는 USDC 계정
    }

    function _salt(address user) internal pure returns (bytes32) { return bytes32(uint256(uint160(user))); }

    function deposit(uint64 amount) external {
        bytes32 self       = Helper.pda(address(this));                   // 서명자: 어댑터의 권한
        bytes32 adapterAta = Helper.ata(address(this), USDC_MINT);
        bytes32 position   = Helper.pda_with_salt(address(this), _salt(msg.sender)); // 사용자별 sub-PDA, 역시 서명

        // 사용자의 토큰 계정에서 끌어옵니다. 어댑터의 권한이 SPL
        // 사용자가 부여한 delegate로 서명합니다; 아래 CPI와 마찬가지로 이것은 레거시 트랙 호출입니다.
        Helper.transfer_spl(Helper.ata(msg.sender, USDC_MINT), adapterAta, amount, USDC_MINT);

        ICrossProgramInvocation.AccountMeta[] memory accs = new ICrossProgramInvocation.AccountMeta[](5);
        accs[0] = ICrossProgramInvocation.AccountMeta(self,       true,  true);
        accs[1] = ICrossProgramInvocation.AccountMeta(position,   true,  true);
        accs[2] = ICrossProgramInvocation.AccountMeta(adapterAta, false, true);                          // 소스: 어댑터의 권한이 소유
        accs[3] = ICrossProgramInvocation.AccountMeta(_vaultFor(position), false, true);                 // 여러분의 프로그램 vault
        accs[4] = ICrossProgramInvocation.AccountMeta(SPL_TOKEN, false, false);

        bytes32[] memory salts = new bytes32[](1);
        salts[0] = _salt(msg.sender);
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeDeposit(amount), salts);
    }

    function withdraw(uint64 amount) external {
        bytes32 position = Helper.pda_with_salt(address(this), _salt(msg.sender));
        bytes32 userAta  = Helper.ata(msg.sender, USDC_MINT);            // 목적지: 사용자의 자체 계정
        // ... 같은 형태: 여러분의 프로그램이 vault에서 userAta로 `amount`를 옮기며, `position`이 서명합니다
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeWithdraw(amount), salts);
    }
}
```

어댑터는 `wUSDC` 계약을 절대 호출하지 않습니다. 끌어오기는 사용자의 delegation을 사용하는 HelperProgram 호출이며, 토큰은 `external_auth(adapter)` 가 소유한 토큰 계정에 놓이므로 CPI가 그것들을 쓸 수 있습니다.

서명자에 대한 메모. Rome은 정확히 두 종류의 계정에 서명합니다: 호출하는 계약의 벌거벗은 권한(`Helper.pda(address(this))`), 이는 `salts`에 나열되지 않아도 서명하며, `salts`의 각 항목마다 하나의 salted sub-PDA입니다. 여러분이 나열한 모든 salt는 서명자로서 계정 목록에 나타나야 하며, 그렇지 않으면 호출은 `제공된 seed로 계산된 signer account를 찾을 수 없음`으로 실패합니다. 사용자의 자체 권한을 포함하여 서명자로 표시된 다른 어떤 계정도 Rome에서 seed를 받지 못하고, Solana runtime은 CPI를 거부합니다. 사용자의 토큰에 대한 권리는 delegation에서 오며, 어댑터가 사용자를 대신해 서명하는 데서 오지 않습니다.

끌어오기 없는 delegation: 여러분의 프로그램이 사용자의 토큰 계정에서 직접 지출해야 한다면, 사용자는 `HelperProgram.approve_spl_raw_delegate(userAta, yourProgramPda, amount, mint, decimals)` 를 HelperProgram으로 직접 보내고, 여러분의 프로그램 PDA를 SPL delegate로 지정합니다. Rome 브리지는 Wormhole에 이것을 사용합니다. 3절의 one-delegate 규칙은 여기에도 적용됩니다: 그 허가는 래퍼의 허가를 대체합니다.

## 8. 테스트

* 다음에 먼저 배포하세요 **Hadrian** (testnet, chain 200010); 주소와 RPC는 [컨트랙트 주소](/ko/reference/contract-addresses.md) 그리고 [레지스트리](https://github.com/rome-protocol/rome-registry).
* `eth_call` 및 `eth_estimateGas` 프록시가 전송할 때 사용하는 것과 같은 에뮬레이션을 실행하세요. 그러면 그곳에서 실패한 CPI는 같은 이유로 체인에서도 실패하고, revert 데이터는 근본 Solana 오류(`non-evm call error: …` 를 Solana 프로그램 로그에서) 전달합니다.
* 착지한 EVM 트랜잭션의 Solana 쪽을 다음으로 읽으세요 `rome_solanaTxForEvmTx(hash)`; Solana 영수증에는 CU와 당신의 프로그램 로그가 있습니다.
* 에서 작동하는 CPI `eth_call` 하지만 전송하면 거부되는 경우는 보통 에뮬레이션이 보고하는 제한에 걸린 것이다: 64개 주소 한도, 원자적 CU 예산(트랜잭션이 반복적으로 실행됨), 또는 track 규칙.

## 9. 참고 자료

* [프리컴파일 참조](/ko/reference/precompiles.md) — 각 프리컴파일의 모든 셀렉터와 해당 디스패치 클래스, 그리고 delegatecall 규칙.
* [제약 사항](/ko/core-concepts/constraints.md) — 위의 제한을 한 표에 정리한 것.
* [토큰 상호운용성](/ko/core-concepts/token-interop.md) — 래퍼, 표준 민트, 예치 및 출금.
* 공개 저장소의 참고 앱: [cardo](https://github.com/rome-protocol/cardo) (사용자가 CPI 프리컴파일을 직접 호출함, 형태 B), [rome-dex](https://github.com/rome-protocol/rome-dex) (직접 `invoke` 그리고 수탁 없는 라우터(형태 C), [aerarium](https://github.com/rome-protocol/aerarium) (하나의 공유 마켓 위에서 동작하는 Compound III 대출 앱; Solidity에서 Solana 프로그램을 호출하지 않으므로(CPI 없음), 어댑터의 예시는 아니다).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.rome.builders/ko/developer-guides/integrate-solana-program.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
