DENYLIST_POLICY
principal(EOA 서명자 또는 ERC-4337 userOp의 스마트 계정 sender)이 설정된 주소 목록에 포함되면 트랜잭션을 거절합니다.
DENYLIST_POLICY는 가장 단순한 PCL 내장 정책 템플릿으로, 자금 이동 트랜잭션을 개시할 수 없는 주소 목록을 담습니다. PCL은 각 호출의 principal(일반 트랜잭션의 EOA 서명자 또는 ERC-4337 userOp의 스마트 계정 sender)을 이 목록과 대조하며, principal이 목록에 있으면 실행 이전 단계에서 InDenylist reason code로 거절합니다. 이 목록은 GlobalPolicyConfig로 체인 전역 규칙으로 부착하거나 ContractPolicyConfig로 컨트랙트 범위 규칙으로 부착할 수 있습니다.
파라미터 struct
템플릿 파라미터 struct는
IPcl.sol에서 그대로 가져옵니다. addresses는 차단할 principal의 단순 목록이며, 별도의 allow-list 필드는 없습니다.struct DenylistPolicy {
address[] addresses;
}
// ABI 튜플: (address[]) PolicySet 만들기
abi.encode로 struct를 인코딩하고 templateId "DENYLIST_POLICY"로 PolicySet에 래핑합니다. selector를 비워두면 대상 컨트랙트의 모든 호출에 규칙이 적용됩니다.import { IPcl, PolicySet, DenylistPolicy } from "@maroo-chain/contracts/IPcl.sol";
address[] memory blocked = new address[](2);
blocked[0] = 0xAaAa000000000000000000000000000000000001; // 프로덕션 전에 교체
blocked[1] = 0xBbBb000000000000000000000000000000000002; // 프로덕션 전에 교체
DenylistPolicy memory dl = DenylistPolicy({ addresses: blocked });
PolicySet memory ps = PolicySet({
templateId: "DENYLIST_POLICY",
policy: abi.encode(dl),
selector: "" // empty = 모든 호출에 적용
}); 전역 vs 컨트랙트 범위
setGlobalPolicies로 부착한 denylist는 나열된 principal이 체인 위에서 어떤 트랜잭션도 개시할 수 없도록 차단합니다. 제재 리스트(체인 전역 정책 관리자가 관리)를 적용하는 전형적인 방식입니다. registerContractPolicies로 부착한 denylist는 해당 컨트랙트를 대상으로 하는 호출만 차단하므로, 전역 상태에 손대지 않고 자체 blocklist를 갖고 싶은 단일 애플리케이션에 유용합니다.principal 판별 — EOA와 ERC-4337 스마트 계정
PCL은 즉시 caller가 아니라 트랜잭션 principal을 평가합니다. 직접 서명된 트랜잭션의 principal은 EOA
from입니다. ERC-4337 userOp의 principal은 userOp.sender에 담긴 스마트 계정 주소이며, handleOps를 제출한 bundler도 userOp에 서명한 owner EOA도 아닙니다. 따라서 스마트 계정 주소를 denylist에 올리면, 어떤 bundler가 브로드캐스트하더라도 그 계정을 sender로 하는 모든 userOp가 차단됩니다.// 스마트 계정 주소를 denylist에 올려 어떤 bundler도 이 계정 명의의 userOp를 제출하지 못하도록 차단합니다.
address[] memory blocked = new address[](1);
blocked[0] = smartAccountAddr; // AA 지갑의 온체인 주소
DenylistPolicy memory dl = DenylistPolicy({ addresses: blocked });
IPcl(0x1000000000000000000000000000000000000005).setGlobalPolicies(
GlobalPolicyConfig({
policies: _wrapAsPolicySet("DENYLIST_POLICY", abi.encode(dl))
})
); 거절이 나타나는 지점 — revert 표면
클라이언트에서 거절이 어떻게 표면화되는지는, denylist에 오른 주소가 트랜잭션 서명자 / userOp.sender(실행 전 필터)인지 아니면 내부 호출의 수신자(실행 후 강제)인지에 따라 달라집니다.
실무 UX 관점: ERC-4337을 감싸는 통합 코드는 실패 경로를 두 가지 다뤄야 합니다. 제출 이전의 RPC 하드 오류와, 체인에 포함되었지만 실패한 handleOps receipt입니다.
| 시나리오 | 거절 지점 | 클라이언트가 보게 되는 것 |
|---|---|---|
EOA from이 denylist에 있는 경우 | 브로드캐스트 필터 | 전송 시 RPC 오류가 발생하며, txhash와 receipt가 모두 없습니다 |
userOp.sender(스마트 계정)가 denylist에 있는 경우 | 브로드캐스트 필터 | 바깥 handleOps의 eth_sendRawTransaction이 RPC 오류로 실패하며, txhash와 receipt가 모두 없습니다 |
| denylist 주소가 내부 호출 수신자나 calldata 인자로만 참조되는 경우 | 실행 후 | handleOps가 status = 0으로 채굴되고 receipt가 존재하며, IPcl ABI로 revert를 디코드하면 InDenylist가 보입니다 |
실무 UX 관점: ERC-4337을 감싸는 통합 코드는 실패 경로를 두 가지 다뤄야 합니다. 제출 이전의 RPC 하드 오류와, 체인에 포함되었지만 실패한 handleOps receipt입니다.
거절 디코드
receipt가 존재하는 경우(실행 후 경로), revert 페이로드를 IPcl ABI로 디코드합니다.
InDenylist는 차단된 sender 주소를 인자로 담고 있으므로, UI에서 어느 주체가 차단되었는지 표시할 수 있습니다.import { decodeErrorResult } from "viem";
try {
await walletClient.writeContract({
address: entryPointAddress,
abi: entryPointAbi,
functionName: "handleOps",
args: [userOps, beneficiary],
});
} catch (err: any) {
// 브로드캐스트 거절 경로: err.data와 receipt가 모두 없습니다.
if (!err?.data) {
console.log("PCL이 브로드캐스트 단계에서 차단했습니다. 서명자 또는 userOp.sender가 denylist에 있을 가능성이 높습니다");
return;
}
// 실행 후 경로: revert 페이로드가 IPcl 오류로 ABI 인코딩되어 있습니다.
const decoded = decodeErrorResult({ abi: pclAbi, data: err.data });
if (decoded.errorName === "InDenylist") {
console.log("차단된 principal:", decoded.args[0]);
}
}