PCL 이중 트랙 트랜잭션 모델
마루의 모든 EVM 호출은 두 트랙 중 하나로 실행됩니다. 직접 호출은 개방 트랙에서 처리되고, 규제 트랙에서는 PCL 래핑 프록시가 실제 실행을 감싸며 PCL 프리컴파일의 preCall과 postCall을 호출합니다.
마루는 트랜잭션 실행을 두 컨텍스트로 분리합니다. 개방 트랙에서는 호출이 대상 컨트랙트로 직접 전달되며, 트랜잭션 진입점에서 평가되는 전역 GlobalPolicyConfig만 적용됩니다. 컨트랙트 범위 정책은 작동하지 않습니다. 규제 트랙에서는 사용자가 PCL에 등록된 프록시 주소로 트랜잭션을 보냅니다. 프록시의 훅 경로가 실제 실행 전에 IPcl.preCall(...)을, 실행 후에 IPcl.postCall(...)을 호출하므로, 프록시에 바인딩된 ContractPolicyConfig가 호출 전후로 원자적으로 강제됩니다. 컨트랙트 관리자는 사용자 대상 진입점으로 구현 컨트랙트 주소(개방)를 공개할지, PCL 등록 프록시 주소(규제)를 공개할지 선택해 트랙을 결정합니다.
아키텍처
flowchart LR
U["User / dApp"]:::evm
IMPL["Implementation contract"]:::evm
PROXY["PCL-registered proxy"]:::evm
PCL["IPcl precompile<br/>0x…0005"]:::precompile
U -->|"open track:<br/>direct call"| IMPL
U -->|"regulated track"| PROXY
PROXY -->|"preCall(...)"| PCL
PROXY -->|"delegatecall"| IMPL
PROXY -->|"postCall(...)"| PCL
classDef evm fill:#0096AA,stroke:#0096AA,color:#fff;
classDef precompile fill:#FF8C50,stroke:#FF8C50,color:#fff; 개방 트랙에서는 사용자가 구현 컨트랙트를 직접 호출하며 전역 정책만 적용됩니다. 규제 트랙에서는 사용자가 PCL 등록 프록시를 호출하고, 프록시의 훅 경로가 구현체 delegatecall 전후로 PCL 프리컴파일의 `preCall`과 `postCall`을 호출합니다.
개방 트랙 — 직접 EVM 호출
to 필드가 일반 컨트랙트 주소인 트랜잭션은 표준 EVM 시맨틱으로 실행됩니다. 트랜잭션 진입 시점의 정책 검사는 전역 GlobalPolicyConfig(denylist, 컨트랙트 간 주기 상한 등)를 여전히 평가하지만, 대상이 PCL 등록 프록시가 아니므로 ContractPolicyConfig는 로드되지 않습니다. 함수별 컴플라이언스 게이트가 필요 없는 유틸리티 컨트랙트에 적합한 트랙입니다.규제 트랙 — PCL 래핑 프록시
규제 트랙은
1.
2. 프록시가 구현 컨트랙트로 위임하여 요청된 함수를 실행합니다.
3.
IPcl.deployPclProxy(...)로 배포되어 PCL 레지스트리에 등록된 프록시 주소를 호출할 때 진입합니다(pcl-proxy 참고). 이러한 프록시를 통한 모든 호출은 PCL 프리컴파일의 두 호출로 감쌉니다.1.
preCall(contractAddress, principal, data, value) — 적용 대상 정책(전역 + 프록시의 ContractPolicyConfig)을 평가하고 session id를 반환합니다.2. 프록시가 구현 컨트랙트로 위임하여 요청된 함수를 실행합니다.
3.
postCall(sessionId, contractAddress, principal, data, value, workable) — 주기 거래량 회계를 마감하고 성공 시 PolicyCheckPassed를 발행합니다.preCall이 revert되면 실제 실행이 수행되지 않습니다. postCall이 revert되면 트랜잭션 전체가 롤백됩니다. 이 블록은 붙여넣어 쓰는 소스가 아니라 형태 스케치입니다. 호출 순서를 보여주는 용도입니다.// 사용자 호출을 감싸는 PCL 래핑 프록시의 훅 경로를 단순화한 스케치입니다.
// 실제 프록시 바이트코드는 체인 바이너리에 내장되어 있으며 IPcl.deployPclProxy로
// 배포됩니다. 사용자가 직접 작성하지 않습니다.
interface IPcl {
function preCall(address contractAddress, address principal, bytes calldata data, uint256 value)
external returns (bytes32 sessionId);
function postCall(bytes32 sessionId, address contractAddress, address principal, bytes calldata data, uint256 value, bool workable)
external returns (bytes memory);
}
address constant PCL = 0x1000000000000000000000000000000000000005;
function _regulatedForward(address impl, bytes calldata data) internal returns (bytes memory) {
bytes32 sid = IPcl(PCL).preCall(address(this), msg.sender, data, msg.value);
(bool ok, bytes memory ret) = impl.delegatecall(data);
IPcl(PCL).postCall(sid, address(this), msg.sender, data, msg.value, ok);
require(ok, "call failed");
return ret;
} 트랙 선택
선택은 최종 사용자가 호출할 주소를 무엇으로 공개하는지로 결정됩니다.
규제 dApp은 보통 표준 구현체(OpenZeppelin ERC20, AMM 풀 등)를 배포하고,
| 트랙 | 사용자 대상 주소 | 전역 정책 | 컨트랙트 정책 |
|---|---|---|---|
| 개방 | 구현 컨트랙트 주소 | 트랜잭션 진입 시 강제 | 평가되지 않음 |
| 규제 | PCL 등록 프록시 주소 | 트랜잭션 진입 시 강제 | preCall/postCall이 강제 |
규제 dApp은 보통 표준 구현체(OpenZeppelin ERC20, AMM 풀 등)를 배포하고,
IPcl.deployPclProxy(...)로 감싸며, IPcl.changeContractPolicies(...)로 프록시에 ContractPolicyConfig를 바인딩하고, 문서와 프런트엔드에 프록시 주소를 공개합니다. 구현 컨트랙트를 직접 호출하면 컨트랙트 범위 규칙을 우회하므로, 프록시 주소를 표준 진입점으로 취급해야 합니다.실패 표면
규제 트랙 실패는 타입 지정 PCL revert로 나타납니다. 경계에서 흔히 관찰되는 ReasonCode는
EasNoAttestationReceived, EasAttestationRevoked, EasAttestationExpired, InDenylist, ExceededPeriodicVolume, VolumeAboveMaxLimit, ExceededAgentTransferLimit입니다. PCL ABI에 대해 decodeErrorResult로 디코드하여 실패를 적절한 사용자 메시지로 연결합니다.import { createWalletClient, http, parseEther, decodeErrorResult } from "viem";
import { pclAbi, erc20Abi } from "./abis";
// TODO: 프로덕션 배포 전 실제 PCL 등록 프록시 주소로 교체하십시오.
const PROXY = "0x8F3ac2B1d9E74c05A6B18FE27Dc4913e5A0F7b62" as const;
const wallet = createWalletClient({ transport: http("https://rpc-testnet.maroo.io") });
try {
await wallet.writeContract({
address: PROXY,
abi: erc20Abi,
functionName: "transfer",
// TODO: 프로덕션 배포 전 실제 수신자 주소로 교체하십시오.
args: ["0x2c7f09B81a6D3FF1e5A0d4c6bC2a8f7E19dc3a4B", parseEther("10000000")],
});
} catch (err: any) {
if (err?.data) {
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
console.error("규제 트랙 거절:", decoded.errorName, decoded.args);
} else {
throw err;
}
}