ERC-6808 키 결합 대체가능 토큰 (Fungible Key Bound Token)
Final · 토큰 · 2023-03-31 작성
ERC-20 토큰에 키 지갑 2개를 묶어 키 지갑이 미리 허용한 조건에서만 전송과 승인이 되게 하는 선택적 보안 표준입니다. 지갑이 뚫려도 자산을 대피시키거나 설정을 초기화할 수 있습니다.
왜 필요한가
셀프 커스터디 지갑은 개인키 하나에 모든 권한이 걸려 있습니다. 시드 구문이나 개인키가 한 번 유출되면 공격자가 토큰을 즉시 옮길 수 있고 되돌릴 방법이 없습니다. 원문에 따르면 2021년 1월부터 2022년 3월까지 미국 연방거래위원회에 4만 6천 건이 넘는 암호화폐 사기 신고가 접수됐고 소비자 피해액은 10억 달러를 넘었습니다. 하드웨어 지갑 같은 분산형 대안은 구매와 배송이 필요해 보급에 한계가 있고 중앙화 서비스는 계정 생성과 개인정보 제공을 요구합니다. 기존 ERC-20 토큰은 지갑이 뚫리는 순간 자산을 지킬 수단이 전혀 없습니다. 이 표준은 지갑이 아니라 자산 자체에 선택적 보안 기능을 넣어 세 지갑 중 하나가 뚫려도 토큰을 지킬 두 번째 기회를 주려는 시도입니다.
구현해야 하는 것
event AccountSecured(address _account, uint256 _amount): 계정이 addBindings로 보안을 활성화했을 때 발생합니다. _amount는 그 시점의 잔액입니다.event AccountResetBinding(address _account): 보유자가 resetBindings로 키 지갑 설정을 초기화했을 때 발생합니다.event SafeFallbackActivated(address _account): 보유자가 safeFallback으로 전 자산을 키 지갑 하나로 옮기기로 했을 때 발생합니다.event AccountEnabledTransfer(address _account, uint256 _amount, uint256 _time, address _to, bool _allFunds): 키 지갑이 allowTransfer로 전송 조건(금액, 시간, 대상, 전액 여부)을 허용했을 때 발생합니다.event AccountEnabledApproval(address _account, uint256 _time, uint256 _numberOfTransfers): 키 지갑이 allowApproval로 승인 가능 시간과 허용 전송 횟수를 설정했을 때 발생합니다.event Ingress(address _account, uint256 _amount): 계정이 새로 토큰 보유자가 됐을 때 발생합니다. _amount는 현재 잔액입니다.event Egress(address _account, uint256 _amount): 계정이 토큰을 전부 보내 보유자가 아니게 됐을 때 발생합니다. _amount는 직전 잔액입니다.function addBindings (address _keyWallet1, address _keyWallet2) external returns (bool): 보유 지갑에 키 지갑 2개를 묶어 계정을 보안 상태로 만듭니다. AccountSecured 이벤트를 반드시 발생시킵니다.function getBindings(address _account) external view returns (AccountHolderBindings memory): 해당 계정에 묶인 키 지갑 2개를 구조체로 돌려줍니다.function resetBindings() external returns (bool): 키 지갑에서 호출해 보유 계정의 키 지갑 설정을 초기화합니다. 키 지갑이 뚫렸을 때 씁니다.function safeFallback() external returns (bool);: 키 지갑에서 호출해 보유 계정의 전 토큰을 다른 키 지갑으로 옮깁니다. 보유 지갑이 뚫렸을 때 씁니다.function allowTransfer(uint256 _amount, uint256 _time, address _to, bool _allFunds) external returns (bool);: 키 지갑에서 호출해 보유 지갑이 할 수 있는 전송의 금액, 유효 시간, 대상 주소를 미리 허용합니다.function getTransferableFunds(address _account) external view returns (TransferConditions memory);: 해당 계정에 설정된 전송 허용 조건을 구조체로 돌려줍니다.function allowApproval(uint256 _time, uint256 _numberOfTransfers) external returns (bool): 키 지갑에서 호출해 보유 지갑이 approve류 함수를 쓸 수 있는 시간과 제3자의 전송 횟수를 허용합니다.function getApprovalConditions(address _account) external view returns (ApprovalConditions memory);: 해당 계정에 설정된 승인 허용 조건(시간, 전송 횟수)을 구조체로 돌려줍니다.function isSecureWallet(address _account) external view returns (bool): 해당 계정이 키 지갑을 묶은 보안 계정인지 여부를 돌려줍니다.function transfer(address _to, uint256 _amount) external returns (bool): 토큰을 전송합니다. 보안 계정인데 allowTransfer로 허용받지 않았다면 되돌려야 합니다.function approve(address _spender, uint256 _amount) external returns (bool): 지출자에게 사용 한도를 승인합니다. 보안 계정은 allowApproval을 먼저 거쳐야 하고 설정된 전송 횟수 제한도 함께 적용됩니다.function increaseAllowance(address _spender, uint256 _addedValue) external returns (bool): 지출자의 허용량을 늘립니다. allowApproval을 거치지 않았거나 허용 시간이 지났으면 되돌려야 합니다.function decreaseAllowance(address _spender, uint256 _subtractedValue) external returns (bool): 지출자의 허용량을 줄입니다. 조건은 increaseAllowance와 같고 차감액이 현재 허용량보다 크면 되돌려야 합니다.function transferFrom(address _from, address _to, uint256 _amount) external returns (bool): 승인받은 지출자가 보유자 대신 토큰을 옮깁니다. 호출할 때마다 남은 허용 전송 횟수를 차감하고 0이 되면 승인을 회수합니다.
심층 설명
3-지갑 구조와 addBindings
토큰을 담은 보유 지갑(Holding Wallet)이 addBindings(_keyWallet1, _keyWallet2)를 호출하면 계정이 보안 상태가 되고 AccountSecured 이벤트가 발생합니다. 이후 자산은 보유 지갑에 있지만 쓰는 권한은 키 지갑이 쥐는 식으로 책임이 나뉩니다. 호출자가 보유자가 아니거나, 이미 보안 상태이거나, 두 키 지갑이 서로 같거나, 키 지갑이 호출자와 같거나, 키 지갑이 제로 주소이거나, 다른 보유 계정의 키 지갑으로 이미 쓰이고 있으면 되돌려야 합니다(SHOULD revert). addBindings를 아예 쓰지 않으면 일반 ERC-20과 같은 기본 동작을 합니다.
allowTransfer의 조건 지정
키 지갑이 transfer에 앞서 allowTransfer(_amount, _time, _to, _allFunds)를 호출해 전송을 허용합니다. _amount가 0이면 금액 제한이 없고 _time이 0이면 시간 제한이 없으며 _to가 제로 주소면 대상 제한이 없습니다. _allFunds가 true면 다른 매개변수와 무관하게 전액을 누구에게나 언제든 보낼 수 있게 허용합니다. 호출자가 키 지갑이 아니거나 _amount가 보유 계정 잔액보다 크면 되돌려야 하고 성공 시 AccountEnabledTransfer 이벤트를 반드시 발생시킵니다. 보안 계정이 이 허용 없이 transfer를 호출하면 전송이 되돌려져야 합니다.
allowApproval과 승인 통제
키 지갑이 allowApproval(_time, _numberOfTransfers)를 호출하면 보유 지갑이 _time 동안 approve, increaseAllowance, decreaseAllowance를 쓸 수 있고 지출자가 할 수 있는 전송 횟수가 _numberOfTransfers로 제한됩니다(0이면 허용량 한도 안에서 무제한). approve를 다시 호출하면 허용량과 허용 전송 횟수가 함께 덮어써집니다. transferFrom은 호출될 때마다 남은 횟수를 차감해 0이 되는지 확인하고 0이 되면 금액 0의 approve로 승인을 회수합니다. 보안 계정이 allowApproval 없이, 또는 설정된 시간이 지난 뒤 approve류 함수를 부르면 되돌려야 합니다.
두 가지 복구 장치: safeFallback과 resetBindings
safeFallback은 보유 지갑이 뚫렸거나 접근을 잃었을 때 키 지갑에서 호출합니다. 보유 계정의 모든 토큰이 호출한 지갑이 아닌 다른 키 지갑으로 이동하고 SafeFallbackActivated 이벤트가 발생합니다. resetBindings는 키 지갑이 뚫렸을 때 키 지갑에서 호출해 묶인 키 지갑들을 제거하고 AccountResetBinding 이벤트를 발생시킵니다. 초기화 후 토큰은 일반 ERC-20처럼 동작하며 보유 지갑이 addBindings를 다시 호출해 새 키 지갑을 묶을 수 있습니다. 두 함수 모두 호출자가 키 지갑이 아니면 되돌려야 합니다.
ERC-20 호환성과 선택적 보안
FKBT는 ERC-20(requires: 20)의 특성을 모두 물려받도록 설계됐습니다. 대체가능 토큰을 쓰는 기존 dApp에서 바로 쓸 수 있게 하기 위한 것으로, 기존 토큰과 지갑은 FKBT 도입의 영향 없이 그대로 동작합니다. allowTransfer와 allowApproval이 성공 시 bool을 반환하고 실패 시 되돌리는 것도 ERC-20과의 일관성을 위한 결정입니다. 또한 호출자는 returns (bool success)의 false를 반드시 처리해야 하며 false가 반환되지 않는다고 가정하면 안 됩니다.
조회 함수와 보유자 추적 이벤트
getBindings는 계정의 키 지갑 2개를, getTransferableFunds는 전송 허용 조건(amount, time, to, allFunds)을, getApprovalConditions는 승인 허용 조건(time, numberOfTransfers)을 구조체로 돌려줍니다. getApprovalConditions의 time은 approve류 함수를 부를 수 있는 기한인 block.timestamp 기준 시각입니다. getNumberOfTransfersAllowed는 지출자에게 남은 전송 횟수를, isSecureWallet은 보안 계정 여부를 알려 줍니다. 계정이 새로 보유자가 되면 Ingress, 전량을 보내 보유자가 아니게 되면 Egress 이벤트가 발생합니다. 참고로 원문 Test Cases에는 함수별 평균 가스 사용량이 기록되어 있습니다(addBindings 154,991 등).
쓸 때 주의할 점
- 키 지갑은 반드시 2개를 동시에 등록: addBindings는 키 지갑 2개를 한 번에 등록합니다. 수수료를 줄이고 사람 실수를 막는 목적도 있지만 여러 개를 자유로이 추가할 수 있으면 safeFallback에서 치명적인 상황이 생길 수 있어 모든 KBT는 보안 활성화 시 3-지갑 체계로 동작합니다.
- safeFallback은 호출한 지갑이 아닌 다른 키 지갑으로 이동: safeFallback을 호출한 키 지갑으로 자산이 가면, 공격자가 그 함수를 불러 전 자산을 가져갈 수 있습니다. 호출하지 않은 다른 키 지갑으로 보내는 설계로 단일 실패 지점을 막습니다.
- resetBindings는 키 지갑만 호출 가능: 보유 지갑이 resetBindings를 부를 수 있다면, 공격자가 보유 지갑 하나만 뚫고 초기화한 뒤 토큰을 즉시 가져갈 수 있습니다. 그래서 초기화 권한은 _keyWallet1과 _keyWallet2에만 있습니다.
- 지갑 2개가 동시에 뚫리면 방어 불가: 3개 지갑 중 2개가 악의적으로 장악되면 소유자가 할 수 있는 일이 없습니다. 다만 1개까지의 유출은 허용되므로, 기존 표준과 달리 두 번째 기회가 주어집니다.
- allowApproval은 드레이너형 공격에 대한 추가 방어: 제3자에게 토큰 사용을 맡길 때 시간(_time)과 전송 횟수(_numberOfTransfers)를 제한해 자산을 빼돌리는 악성 dApp 같은 흔한 공격에 노출되는 범위를 줄입니다.