EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer
EIP-712 使用详解
之前的文章我们介绍过如何对数据进行签名,利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单,一般就是给一串字符串,或者一串哈希签名,如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题,利用 EIP-712,我们可以对更大的数据集,例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap,PancakeSwap 等 DEX 的朋友应该有印象,在移除 LP 流动性的时候,我们需要先签名,然后再发送一笔交易移除流动性。正常情况下,其实应该我们先调用 LP 代币的授权方法,授权 DEX 合约可以转移我们的 LP,然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次,就可以将两步交易合并为一步交易,从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义,是一个与域相关的结构体,总共包含五个字段:name,合约或者协议的名称version,合约的版本chainId,合约部署的链 Id,一般使用 ...
流动性挖矿-合约原理详解
流动性挖矿应该是上个牛市最火热的内容,基本上整个 DeFi 都是在围绕着流动性挖矿展开的,今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么,实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如,SushiSwap 最初推出的 DEX 流动性挖矿,用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢?我们今天就来研究一下这部分内容。 先来看几个例子: 一:假设有一个流动性挖矿的合约,可以质押 A token 赚取 B token。它在 0 秒时开始活动,每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token,并且之后没有其他人参与,在第 8 秒时取出 token,图示:那么他在此时获得的收益就是:5R = (2 / 2) * (8 - 3) * R 其中,第一个 2 是用户 A 质押的数量,第二个 2 是合约中质押的总量,(8-3)是用户 ...
CREATE2 操作码使用方法详解
CREATE2 是一个可以在合约中创建合约的操作码。我们先来举个例子看看它能干什么:这段代码是 Uniswap v2-core 里面的工厂合约代码,使用 create2 操作码创建了 pair 合约,返回值是 pair 的地址,这样就可以逻辑中直接使用其地址进行接下来的操作。 那么 create2 到底是怎么使用呢,根据官方 EIP 文档,create2 一共接收四个参数,分别是:endowment(创建合约时往合约中打的 ETH 数量)memory_start(代码在内存中的起始位置,一般固定为 add(bytecode, 0x20) )memory_length(代码长度,一般固定为 mload(bytecode) )salt(随机数盐)这里要注意的是第一个参数如果大于 0 的话,需要待部署合约的构造方法带有 payable。随机数盐是由用户自定,须为 bytes32 格式,例如在上面 Uniswap 的例子中,salt 为:bytes32 salt = keccak256(abi.encodePacked(token0, token1)); create2 还有一个优点,相...
Smart Contract Developer

Subscribe to xyyme.eth

Subscribe to xyyme.eth
Share Dialog
Share Dialog
<100 subscribers
<100 subscribers
XCarnival 是一个 NFT 借贷协议,用户可以将 NFT 抵押给协议,从而借出 token。同时也可以将 token 抵押给协议,获得利息收益。
这次攻击事件的原理是,用户在将 NFT 从协议中取出后,仍然可以借出 token。黑客利用这个 bug,不断新建合约,将其作为抵押人,抵押 NFT 并取出,然后再去借出 token,从而实现无本套利。
XCarnival 的核心在于三个合约:
XNFT,用户抵押 NFT 入口
XToken,用户借款入口
P2Controller,校验用户的权限,例如订单是否可借出,用户是否可赎回等
这次的 bug 在于,用户可以将 NFT 抵押到协议之后,再取出 NFT,而由于合约借贷逻辑中,没有订单的状态进行判断,因此仍然可以调用借出方法进行借贷,造成资产流失。
我们先来看看抵押的代码:

注意到抵押方法的 xToken 地址是作为参数传进去的,指定要借哪个 token。
再来看看取出 NFT 的方法(由于这段代码太长,我们只截取最重要的部分):


我们看到,在取出 NFT 的最后,将订单的 isWithdraw 置为了 true,指明订单已经被取出。
接下来我们看看借出 xToken 的部分:

xToken 中会调用 controller 的各种 allow,verify 方法等来判断该 token 是否可借出。那么我们再看看 controller 中是如何实现的:

注意到没有,这里并没有对订单中的 NFT 是否取出,也就是上面的 isWithdraw 进行检查。如果我们先去抵押 NFT,这时已经生成了订单,然后再取出。此时再去调用 xToken 的 borrow 方法,那么就可以借款,而此时是没有任何抵押的。此次黑客事件就是利用了这个 bug。
我们来看看黑客的几笔交易都在干什么:
其中第四步的详细逻辑为:

这样在一笔交易中就可以构建多个抵押人(即新建的合约)。在实际的攻击中每一笔交易新建了四个内部合约,然后多次调用该方法,就可以构建出许多的抵押人。并且这些抵押人在协议中已经没有了抵押,但是仍然有借出 token 的权利。
接下来第五步,利用内部合约抵押人的身份,在没有任何抵押的情况下,直接从协议中借出 token,完成套利。
注意黑客在调用抵押方法 pledgeAndBorrow 的时候,传入了自定义的 xToken 地址。由于在这一步只需要实现抵押,不需要借贷,因此传入自定义的 xToken,可以避免 xToken 自带的一堆校验。
整体的逻辑还是比较简单的,我们也来尝试写写攻击合约。
要重现攻击,就需要模拟主网的各种状态,这时我们可以利用 hardhat 的 fork 功能,不熟悉的朋友可以看看我之前写的一篇文章,对其用法有详细的解释。
黑客的第一笔交易发生在区块 15028718 中,是购买 BAYC 的交易,那么我们就 fork 区块 15028720,并且使用 hardhat_impersonateAccount 模拟黑客的地址,那么此时在本地我们就拥有了 BAYC(5110),只需要关注攻击逻辑本身即可。
根绝我们前面的分析,在第四步中,需要新建内部的辅助合约,并具有抵押、取出、转移 NFT 的功能,最重要的,需要有借出 xToken 的功能。同时,在外部的攻击合约中,需要维护一个内部合约的地址列表,方便后续集中调用借款方法。
综上,我们最终实现的内部辅助合约为:

外部攻击合约为:

感兴趣的朋友可以多看几遍,结合注释好好理解一下,我们不再详细解释。
接着我们在本地自测,调用两次 prepare 之后调用 attack,测试结果为:
balance before hack is 27.69746933937151467 balance after hack is 315.51434111624879089
差值恰好是 36 * 4 * 2 = 288,说明我们模拟成功。
这次的 bug 原因在于在一个很小的细节,也就是 NFT 取出后没有修改状态。这种小问题,审计人员也没有查出来,说明细节真的很重要。我们在编写合约的时候,对各个状态的变更都要仔细考虑几遍。
欢迎和我交流
XCarnival 是一个 NFT 借贷协议,用户可以将 NFT 抵押给协议,从而借出 token。同时也可以将 token 抵押给协议,获得利息收益。
这次攻击事件的原理是,用户在将 NFT 从协议中取出后,仍然可以借出 token。黑客利用这个 bug,不断新建合约,将其作为抵押人,抵押 NFT 并取出,然后再去借出 token,从而实现无本套利。
XCarnival 的核心在于三个合约:
XNFT,用户抵押 NFT 入口
XToken,用户借款入口
P2Controller,校验用户的权限,例如订单是否可借出,用户是否可赎回等
这次的 bug 在于,用户可以将 NFT 抵押到协议之后,再取出 NFT,而由于合约借贷逻辑中,没有订单的状态进行判断,因此仍然可以调用借出方法进行借贷,造成资产流失。
我们先来看看抵押的代码:

注意到抵押方法的 xToken 地址是作为参数传进去的,指定要借哪个 token。
再来看看取出 NFT 的方法(由于这段代码太长,我们只截取最重要的部分):


我们看到,在取出 NFT 的最后,将订单的 isWithdraw 置为了 true,指明订单已经被取出。
接下来我们看看借出 xToken 的部分:

xToken 中会调用 controller 的各种 allow,verify 方法等来判断该 token 是否可借出。那么我们再看看 controller 中是如何实现的:

注意到没有,这里并没有对订单中的 NFT 是否取出,也就是上面的 isWithdraw 进行检查。如果我们先去抵押 NFT,这时已经生成了订单,然后再取出。此时再去调用 xToken 的 borrow 方法,那么就可以借款,而此时是没有任何抵押的。此次黑客事件就是利用了这个 bug。
我们来看看黑客的几笔交易都在干什么:
其中第四步的详细逻辑为:

这样在一笔交易中就可以构建多个抵押人(即新建的合约)。在实际的攻击中每一笔交易新建了四个内部合约,然后多次调用该方法,就可以构建出许多的抵押人。并且这些抵押人在协议中已经没有了抵押,但是仍然有借出 token 的权利。
接下来第五步,利用内部合约抵押人的身份,在没有任何抵押的情况下,直接从协议中借出 token,完成套利。
注意黑客在调用抵押方法 pledgeAndBorrow 的时候,传入了自定义的 xToken 地址。由于在这一步只需要实现抵押,不需要借贷,因此传入自定义的 xToken,可以避免 xToken 自带的一堆校验。
整体的逻辑还是比较简单的,我们也来尝试写写攻击合约。
要重现攻击,就需要模拟主网的各种状态,这时我们可以利用 hardhat 的 fork 功能,不熟悉的朋友可以看看我之前写的一篇文章,对其用法有详细的解释。
黑客的第一笔交易发生在区块 15028718 中,是购买 BAYC 的交易,那么我们就 fork 区块 15028720,并且使用 hardhat_impersonateAccount 模拟黑客的地址,那么此时在本地我们就拥有了 BAYC(5110),只需要关注攻击逻辑本身即可。
根绝我们前面的分析,在第四步中,需要新建内部的辅助合约,并具有抵押、取出、转移 NFT 的功能,最重要的,需要有借出 xToken 的功能。同时,在外部的攻击合约中,需要维护一个内部合约的地址列表,方便后续集中调用借款方法。
综上,我们最终实现的内部辅助合约为:

外部攻击合约为:

感兴趣的朋友可以多看几遍,结合注释好好理解一下,我们不再详细解释。
接着我们在本地自测,调用两次 prepare 之后调用 attack,测试结果为:
balance before hack is 27.69746933937151467 balance after hack is 315.51434111624879089
差值恰好是 36 * 4 * 2 = 288,说明我们模拟成功。
这次的 bug 原因在于在一个很小的细节,也就是 NFT 取出后没有修改状态。这种小问题,审计人员也没有查出来,说明细节真的很重要。我们在编写合约的时候,对各个状态的变更都要仔细考虑几遍。
欢迎和我交流
No activity yet