유니스왑은 2020년 5월 18일 이더리움 메인넷에서 V2 출시를 발표한 데 이어 2021년 5월 5일에 V3를 출시했습니다. 총 가치 고정(TVL)이 74억 달러(2022년 1월 21일 기준)인 유니스왑 V2는 전체 유니스왑 TVL(36억 달러)의 절반이 조금 넘는 비중을 차지하며, 일평균 거래량은 약 5억 달러에 달합니다. 유니스왑 V2는 탈중앙 금융 사용자들 사이에서 여전히 매우 인기 있는 탈중앙 거래소(DEX)임이 분명합니다.
최근 유니스왑 V2의 콘트랙트를 검토한 결과, 동료 탈중앙 금융 스마트 콘트랙트 개발자와 공유할 만한 가치가 있는 주옥같은 콘트랙트를 발견했습니다.
컨트랙트에 대한 간략한 개요
유니스왑 V2 콘트랙트는 V2 코어와 V2 페리페리로 구성됩니다. V2 코어는 유니스왑V2페어, 유니스왑V2팩토리, 유니스왑V2ERC20을 포함한 필수 V2 스마트 콘트랙트로 구성됩니다. V2 페리퍼리는 일련의 도우미 콘트랙트 또는 기능으로 구성되며, 여기에는 UniswapV2Migrator, UniswapV2Router 및 여러 라이브러리가 포함됩니다.
UniswapV2 core
UniswapV2Pair - 유동성 풀과 토큰 스와핑 기능의 유지 관리를 담당한다.
UniswapV2Factory - 토큰 페어 유동성 풀 생성 및 유동성 공급자 수수료 설정을 담당합니다.
UniswapV2ERC20 - ERC20 토큰 인터페이스를 구현하지만 추가 기능이 있다. (permit)
UniswapV2 periphery
UniswapV2Router01 및 02 - 유동성 추가 및 제거 작업과 토큰 스와핑 기능을 처리하는 V2Pair의 래퍼 역할을 합니다.
UniswapV2Migrator - 유니스왑 V1에서 V2로 유동성 마이그레이션 서비스를 제공합니다.
소스 코드 검토
다음은 유니스왑V2에 구현된 몇 가지 흥미로운 팁과 요령입니다.
bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;
function permit(
address owner,
address spender,
uint value,
uint deadline,
uint8 v,
bytes32 r,
bytes32 s
)
external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
표준 ERC-20 토큰을 사용하면 토큰 소유자는 전송발신 트랜잭션을 실행하기 전에 트랜잭션 승인을 실행해야 합니다. 승인 기능은 외부 소유 지갑(EOA)에서 수행해야 하며, 이는 악의적인 컨트랙트에 의해 사용자의 지갑이 유출되는 것을 방지하기 위한 것입니다. 그러나 이는 또한 사용자가 토큰을 전송하기 위해 approve과 transferFrom이라는 두 가지 트랜잭션을 실행해야 한다는 것을 의미하므로 사용자 경험의 저하로 이어지며, 이는 가스 효율적이지 않습니다. 그렇다면 사용자 자금에 대한 보안을 유지하면서 가스 효율이 더 높은 ERC20 토큰 버전을 구현할 수 있을까요?
솔루션: 유니스왑은 허가(Permit) 기능을 추가하여 가스를 사용하지 않는 토큰 전송 승인을 위한 오프라인 서명을 구현합니다. 그런 다음 서명은 transferFrom 함수(또는 다른 함수)의 온체인 실행에 사용될 수 있습니다. 허가 함수는 사용자가 유동성 풀에서 유동성을 인출할 수 있도록 V2 Router 콘트랙트에서도 사용됩니다(예: removeLiquidityWithPermit 함수).
동일한 서명을 여러 번 사용하는 리플레이 공격을 방지하기 위해(오프체인에서 수행되는 경우), 유효한 서명은 DOMAIN_SEPARATOR 및 PERMIT_TYPEHASH에 정의된 서명 범위의 명확한 정의와 모든 서명에 따라 증가하는 재사용 불가능한 논스와 같은 몇 가지 안전장치로 구성됩니다. 서명은 서명을 위해 EIP-712 표준을 사용하며, 권한 확장의 ERC-20 구현은 EIP-2612 표준을 따릅니다.
ERC-20-퍼밋을 구현하거나 더 자세히 알고 싶은 스마트 컨트랙트 개발자는 아래 리소스를 참조하시기 바랍니다:
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
EVM 호환 체인에서 새로운 콘트랙트를 생성하는 방법에는 CREATE와 CREATE2의 두 가지가 있습니다. 유니스왑V2페어 콘트랙트에서는 (어셈블리에서) CREATE2 옵코드가 토큰 페어 유동성 풀 주소를 생성하는 데 사용되며, 이는 (CREATE가 사용되는) V1 콘트랙트에서 변경된 것입니다.
배포된 컨트랙트 주소를 계산할 때 CREATE 연산자는 다음과 같이 사용됩니다:
keccak256(senderAddress, nonce)
솔리디티에서는 아래와 같이 새로운 키워드를 통해 새로운 컨트랙트를 생성할 수도 있습니다. 이러한 방식으로 새 컨트랙트를 인스턴스화할 때는 CREATE 연산자를 사용하며, 논스를 사용하기 때문에 컨트랙트 주소는 결정론적 방식으로 미리 결정할 수 없습니다.
pair = new UniswapV2Pair()
CREATE2 OP코드는 다음과 같이 미리 결정된 컨트랙트 주소를 계산합니다:
keccak256(0xFF, senderAddress, salt, bytecode)
CREATE2와 CREATE의 장점은 CREATE2를 사용하면 컨트랙트 주소를 오프체인에서 미리 계산할 수 있으며(따라서 가스가 필요 없음), 컨트랙트가 온체인에 배포되기 전에도 미리 계산된 컨트랙트 주소로 이더를 전송할 수 있다는 점입니다. 이러한 사용 사례 중 하나는 개발자가 컨트랙트를 배포하지 않고도 미리 계산된 컨트랙트 주소를 사용자에게 배포할 수 있으며, 사용자가 자금을 전송하기 시작할 때 컨트랙트를 초기화하여 가스 수수료를 절약할 수 있다는 것입니다.
CREATE2를 사용하여 컨트랙트를 배포하는 방법에 대해 자세히 알아보려면 아래 리소스를 참조하세요:
import './libraries/UQ112x112.sol';
// update reserves and, on the first call per block, price accumulators
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
blockTimestampLast = blockTimestamp;
emit Sync(reserve0, reserve1);
}
유니스왑V2 Pair 콘트랙트에서 토큰 가격은 시간 가중 평균 가격(TWAP) 메커니즘에 따라 업데이트됩니다. 작동 방식은 각 거래 가격에 업데이트된 마지막 블록 이후 경과한 시간에 따라 가중치를 부여한 다음 과거 가격을 누적하는 것입니다. 이는 난이도를 높이고 특히 거래량이 많은 토큰의 경우 유니스왑에서 가격을 조작하려는 인센티브를 줄입니다. 이는 유니스왑 V1에서 개선된 점입니다. 아래 다이어그램은 TWAP 가격 메커니즘에 대한 좋은 개요를 제공합니다(유니스왑 제공).
가격 조작에 더 강한 V2의 가격 업데이트 메커니즘이 개선되었기 때문에, 유니스왑 V2는 다른 디앱의 온체인 가격 오라클 역할을 할 수 있습니다. 실제로 유니스왑은 이러한 목적을 위해 유니스왑V2OracleLibrary를 만들었습니다.
시사점
토큰 가격은 유동성 풀의 실제 준비금 잔액을 기준으로 업데이트되지 않으므로 TWAP 메커니즘에 따라 계산된 토큰 잔액과 실제 준비금 잔액 사이에 불일치가 발생할 수 있습니다. 또한 디플레이션 또는 인플레이션을 구현하는 토큰도 있어 토큰 잔액에 불일치가 발생할 수 있습니다. 잔액이 2^112(2의 112승)보다 큰 토큰도 준비금 저장 슬롯이 넘쳐 거래 실패로 이어질 수 있습니다. 위에 언급된 모든 경우(전부는 아님)에 대해 사용자는 skim 및 sync 기능을 통해 초과 토큰 잔액 또는 준비금을 인출할 수 있습니다.
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
문제: 솔리디티는 정수가 아닌 숫자 유형(예: 분수)을 지원하지 않습니다.
해결책: 위의 문제를 극복하기 위해 유니스왑 V2는 위의 라이브러리를 사용하여 가격을 업데이트할 때 고정 소수점 이진수를 처리합니다. 고정 소수점 이진수의 경우 범위와 정밀도 사이에 상충 관계가 있으며, 숫자의 범위가 클수록 정밀도가 낮아집니다. 유니스왑의 경우 고정 소수점 이진수는 uint224로 구현되며, 처음 112 비트는 정수를 나타내고 마지막 112 비트는 소수를 나타냅니다. 즉, 숫자의 범위는 [0, 2^112-1]이고 정밀도는 1⁄112입니다.
고정 소수점 이진수에 대해 더 자세히 알고 싶은 독자는 아래 동영상에서 훌륭한 설명을 참조하시기 바랍니다:
uint private unlocked = 1;
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
유니스왑은 재진입 공격을 방지하기 위한 조치로 잠금 수정자를 사용합니다. 가장 유명한 재진입 공격은 2016년에 발생했으며, 공격자는 DAO에서 360만 개의 이더를 탈취했습니다.
잠금 수정자는 잠금 해제 상태 변수의 초기값을 1로 설정하여 재진입 공격을 방지합니다. 잠금 수정자가 있는 함수가 실행되면 잠금 해제된 상태 변수 값이 0으로 설정됩니다. 컨트랙트에 재진입하려면 잠금 해제된 상태 변수가 == 1이어야 하므로 재진입을 방지할 수 있습니다. 잠금 해제된 상태 변수 값은 함수 실행에 성공하거나 실행에 실패하면(원래 상태로 되돌아감) 1로 돌아갑니다. 유니스왑은 Mint, Burn, Swap, Skim과 같이 외부 호출을 수행하는 모든 유니스왑 V2Pair 함수에 Lock 수정자를 적용합니다.
개발자는 재진입 가드와 유사한 구현을 위해 오픈제플린 컨트랙트 스위트를 참조할 수도 있습니다.
추가 리소스
다음 출처의 글이 원본 글이다.
출처: https://dev.to/francisldn/5-tips-tricks-in-uniswapv2-contracts-for-defi-developers-32oa
Uniswap v3에 대하여 (0) | 2023.04.27 |
---|---|
The Uniswap V3 Smart Contracts (0) | 2023.04.12 |
UniswapV2-core - UniswapV2ERC20.sol 코드 분석 (0) | 2023.03.26 |
UniswapV2-core - UniswapV2Factory.sol 코드 분석 (0) | 2023.03.26 |
UniswapV2-core - UniswapV2Pair.sol 코드 분석 (0) | 2023.03.26 |