market
分析
1.全局观
存储、业务逻辑、市场相分离,形成三个合约
- EternalStorage.sol:存储合约,任何调用都需要在fallback中执行。有owner机制
- Market.sol
- CryptoCollectibles:业务逻辑合约,有owner机制,mint、转让、授权代币功能
- CryptoCollectiblesMarket:市场合约,token的持有者可以在此合约买卖token,但是需要支付手续费。mint代币的钱、手续费都是放在市场合约,存储合约、业务逻辑合约都负责收钱。任何人都可以mint代币,并且卖出去的时候又收回钱,也就是说成本只有一点手续费而已。
- Setup:初始化题目
2.任务
将市场合约的余额为0
1 | function isSolved() external view returns (bool) { |
3.详细分析
先来看资产和状态情况:
ETH | 持币 | 授权 | 状态 | |
---|---|---|---|---|
Setup | ||||
市场合约 | 50 | owner=Setup | ||
业务逻辑合约 | owner=Setup, minter[Setup,市场] | |||
存储合约 | owner=业务逻辑 | |||
随机地址1 | 0: ~5 ether | |||
随机地址2 | 0: ~10 ether | |||
随机地址3 | 0: ~15 ether | |||
随机地址4 | 0: ~20 ether |
如果能拿到四个随机地址的私钥,就可以卖出了,但是这行不通,并且市场合约中还残留了一些手续费。因此大方向应该是:清理合约的代币价值、清理合约手续费。
存储合约的owner拥有最高权限,可以操作任何人的token,如果拿到这个权限则本题可迎刃而解。
- 存储合约的owner是业务逻辑合约,但是他没有转让的逻辑,并且方法是写死的,因此没有方法可以让逻辑合约调用存储合约的转让所有权方法。同时也没有修改slot的方法。不可行。
- 既然没办法让逻辑合约主动调用转让方法,那修改存储合约的owner只能靠修改slot了。我们发现存储合约的
updateName(bytes32,bytes32)
可以修改slot的内容(前提得你是一个token的所有者),但位置我们无法控制,其他方法也是类似。不可行。 - 因此修改slot数据这条路也走不通。
逻辑合约似乎写的无懈可击
钱是存储在市场合约的,看看能不能直接拿走,也许买卖逻辑写的有问题
有很多题目的考点都是出在卖出这个方法,因为写得不严谨,导致可以卖多次或者可以凭空卖出。
1
2
3
4
5
6
7
8
9
10
11
12
13function sellCollectible(bytes32 tokenId) public payable {
require(tokenPrices[tokenId] > 0, "sellCollectible/not-listed");
(, address tokenOwner, address approved, ) = cryptoCollectibles.getTokenInfo(tokenId);
require(msg.sender == tokenOwner, "sellCollectible/not-owner");
// 卖之前需要approve给此合约
require(approved == address(this), "sellCollectible/not-approved");
cryptoCollectibles.transferFrom(tokenId, msg.sender, address(this));
// 然后此合约转钱
msg.sender.transfer(tokenPrices[tokenId]);
}从这个方法中我们可以发现,先查看这个代币是不是被挖出来过,如果是则用
getTokenInfo()
去到存储合约查看该token的信息,经过验证之后就转钱。这就涉及到了storage布局的问题了,如果我们修改storage就可以做了,但是从上面的分析可以知道我们无法直接凭空修改storage信息。但是还有一种考点就是让两个结构体之间的storage分布重叠,这样也可以达到修改storage的目的。这道题的结构体有四个属性,并且token的所有者可以任意修改这四个属性。这就有操作空间了。
1
2
3
4
5
6struct TokenInfo {
bytes32 displayName;
address owner;
address approved;
address metadata;
}那么,如果我们mint一个自己的token,这样我们就可以操作storage了,但是怎么操作呢?两条路:
- 多次卖出自己的token
- 修改整个EVM的storage
由于我们的tokenId是随机的不可控,因此很说想修改哪个storage的内容就修改哪个,因此我们只能看看能不能多次卖出自己的token。我们可以修改自己的这四个属性的storage,范围属实很小。再次分析
sellCollectible()
看看有没有什么操作空间。我们发现
require(tokenPrices[tokenId] > 0, "sellCollectible/not-listed");
仅仅检查是不是被挖过,然后就检查存储合约的storage布局。当我们修改了我们的token的四个属性之后,用另外一个没有mint过的tokenId直接去调用存储合约的方法,利用结构体重叠的方式,使得这个没有被mint过的token也变得有效,然后重复操作重叠的内容,使得原来我们有效的token可以重复卖出
从存储合约的逻辑中,我们可以看出,它是根据tokenId后面之后的内容判断一个token的所有者是谁,如果重叠了了之后,就可以修改一个token的所有者。我们将如下这么重叠,通过修改tokenId_0的approval和metadata属性会修改tokenId_1的所有者属性。
mint过有效的tokenId_0 | 未mint的tokenId_1 |
---|---|
tokenId_0.name | |
tokenId_0.owner | |
tokenId_0.approval | tokenId_1.name |
tokenId_0.metadata | tokenId_1.owner |
tokenId_1.approval | |
tokenId_1.metadata |
因此,我们有了具体的解决思路:
- mint一个有效的token,然后修改此token的四个属性
- 直接与存储合约交互,由于结构体重叠,使得原本没被mint过的token也有效
- 重复操作重叠部分的内容,使得我们可以重复卖出原本有效的token
- 因为有手续费的原因,我们需要额外的操作来处理残留的手续费
解题
1 | // SPDX-License-Identifier: UNLICENSED |
1 | Logs: |