அழைப்புத் தரவு (Calldata) உகப்பாக்கத்திற்கான குறுகிய ABI-கள்
அறிமுகம்
இந்தக் கட்டுரையில், நீங்கள் ஆப்டிமிஸ்டிக் ரோலப்கள் (optimistic rollups), அவற்றில் பரிவர்த்தனைகளுக்கான செலவு மற்றும் அந்த மாறுபட்ட செலவு அமைப்பு எத்தேரியம் முதன்மை வலைப்பின்னலில் (Ethereum Mainnet) உள்ளதை விட வெவ்வேறு விஷயங்களுக்கு எவ்வாறு உகப்பாக்கக் கோருகிறது என்பதைப் பற்றி அறிந்துகொள்வீர்கள். இந்த உகப்பாக்கத்தை எவ்வாறு செயல்படுத்துவது என்பதையும் நீங்கள் அறிந்துகொள்வீர்கள்.
முழு வெளிப்படுத்தல்
நான் ஒரு முழுநேர ஆப்டிமிசம் (opens in a new tab) ஊழியர், எனவே இந்தக் கட்டுரையில் உள்ள எடுத்துக்காட்டுகள் ஆப்டிமிசத்தில் இயங்கும். இருப்பினும், இங்கு விளக்கப்பட்டுள்ள நுட்பம் மற்ற ரோலப்களுக்கும் (rollups) அதேபோன்று சிறப்பாகச் செயல்பட வேண்டும்.
கலைச்சொற்கள்
ரோலப்களைப் பற்றி விவாதிக்கும்போது, 'அடுக்கு 1 (l1)' என்ற சொல் தயாரிப்பு எத்திரியம் பிணையமான முதன்மை வலைப்பின்னலுக்குப் பயன்படுத்தப்படுகிறது. 'அடுக்கு 2 (l2)' என்ற சொல் ரோலப் அல்லது பாதுகாப்பிற்காக L1-ஐச் சார்ந்திருக்கும் ஆனால் அதன் பெரும்பாலான செயலாக்கத்தைப் புறச்சங்கிலியில் (offchain) செய்யும் வேறு எந்த அமைப்புக்கும் பயன்படுத்தப்படுகிறது.
L2 பரிவர்த்தனைகளின் செலவை நாம் எவ்வாறு மேலும் குறைக்கலாம்?
ஆப்டிமிஸ்டிக் ரோலப்கள் ஒவ்வொரு வரலாற்றுப் பரிவர்த்தனையின் பதிவையும் பாதுகாக்க வேண்டும், இதனால் எவரும் அவற்றைச் சரிபார்த்து தற்போதைய நிலை (state) சரியானது என்பதை உறுதிப்படுத்த முடியும். எத்தேரியம் முதன்மை வலைப்பின்னலில் தரவைப் பெறுவதற்கான மலிவான வழி, அதை அழைப்புத் தரவாக (calldata) எழுதுவதாகும். இந்தத் தீர்வு ஆப்டிமிசம் (opens in a new tab) மற்றும் ஆர்பிட்ரம் (opens in a new tab) ஆகிய இரண்டாலும் தேர்ந்தெடுக்கப்பட்டது.
L2 பரிவர்த்தனைகளின் செலவு
L2 பரிவர்த்தனைகளின் செலவு இரண்டு கூறுகளைக் கொண்டுள்ளது:
- L2 செயலாக்கம், இது பொதுவாக மிகவும் மலிவானது
- L1 சேமிப்பகம், இது முதன்மை வலைப்பின்னல் எரிவாயு (gas) செலவுகளுடன் பிணைக்கப்பட்டுள்ளது
நான் இதை எழுதும்போது, ஆப்டிமிசத்தில் L2 எரிவாயுவின் செலவு 0.001 Gwei ஆக உள்ளது. மறுபுறம், L1 எரிவாயுவின் செலவு தோராயமாக 40 Gwei ஆகும். தற்போதைய விலைகளை நீங்கள் இங்கே பார்க்கலாம் (opens in a new tab).
ஒரு பைட் அழைப்புத் தரவின் செலவு 4 எரிவாயு (அது பூஜ்ஜியமாக இருந்தால்) அல்லது 16 எரிவாயு (அது வேறு எந்த மதிப்பாக இருந்தாலும்) ஆகும். EVM-இல் மிகவும் விலையுயர்ந்த செயல்பாடுகளில் ஒன்று சேமிப்பகத்தில் எழுதுவதாகும். L2-இல் சேமிப்பகத்தில் 32-பைட் சொல்லை எழுதுவதற்கான அதிகபட்ச செலவு 22100 எரிவாயு ஆகும். தற்போது, இது 22.1 Gwei ஆக உள்ளது. எனவே, அழைப்புத் தரவின் ஒரு பூஜ்ஜிய பைட்டை நம்மால் சேமிக்க முடிந்தால், நம்மால் சுமார் 200 பைட்டுகளைச் சேமிப்பகத்தில் எழுத முடியும், மேலும் லாபகரமாகவும் இருக்க முடியும்.
ABI
பெரும்பாலான பரிவர்த்தனைகள் வெளிப்புறமாகச் சொந்தமான கணக்கிலிருந்து (externally-owned account) ஒரு ஒப்பந்தத்தை அணுகுகின்றன. பெரும்பாலான ஒப்பந்தங்கள் Solidity-இல் எழுதப்பட்டுள்ளன, மேலும் அவற்றின் தரவுப் புலத்தை பயன்பாட்டு பைனரி இடைமுகத்தின் (ABI) (opens in a new tab) படி விளக்குகின்றன.
இருப்பினும், ABI ஆனது L1-க்காக வடிவமைக்கப்பட்டது, அங்கு ஒரு பைட் அழைப்புத் தரவின் செலவு தோராயமாக நான்கு எண்கணித செயல்பாடுகளுக்குச் சமமாக இருக்கும், ஆனால் L2-இல் ஒரு பைட் அழைப்புத் தரவின் செலவு ஆயிரத்திற்கும் மேற்பட்ட எண்கணித செயல்பாடுகளை விட அதிகமாகும். அழைப்புத் தரவு இவ்வாறு பிரிக்கப்பட்டுள்ளது:
| பிரிவு | நீளம் | பைட்டுகள் | வீணான பைட்டுகள் | வீணான எரிவாயு | தேவையான பைட்டுகள் | தேவையான எரிவாயு |
|---|---|---|---|---|---|---|
| செயல்பாடுத் தேர்வி | 4 | 0-3 | 3 | 48 | 1 | 16 |
| பூஜ்ஜியங்கள் | 12 | 4-15 | 12 | 48 | 0 | 0 |
| இலக்கு முகவரி | 20 | 16-35 | 0 | 0 | 20 | 320 |
| தொகை | 32 | 36-67 | 17 | 64 | 15 | 240 |
| மொத்தம் | 68 | 160 | 576 |
விளக்கம்:
- செயல்பாடுத் தேர்வி: ஒப்பந்தத்தில் 256-க்கும் குறைவான செயல்பாடுகள் உள்ளன, எனவே அவற்றை ஒரு பைட்டைக் கொண்டு வேறுபடுத்தலாம். இந்தப் பைட்டுகள் பொதுவாகப் பூஜ்ஜியமற்றவை, எனவே பதினாறு எரிவாயு செலவாகும் (opens in a new tab).
- பூஜ்ஜியங்கள்: இருபது பைட் முகவரியை வைத்திருக்க முப்பத்திரண்டு பைட் சொல் தேவையில்லை என்பதால் இந்தப் பைட்டுகள் எப்போதும் பூஜ்ஜியமாகவே இருக்கும்.
பூஜ்ஜியத்தைக் கொண்டிருக்கும் பைட்டுகளுக்கு நான்கு எரிவாயு செலவாகும் (மஞ்சள் அறிக்கையைப் பார்க்கவும் (opens in a new tab), பிற்சேர்க்கை G, ப. 27,
Gtxdatazero-க்கான மதிப்பு). - தொகை: இந்த ஒப்பந்தத்தில்
decimalsபதினெட்டு (வழக்கமான மதிப்பு) என்றும், நாம் பரிமாற்றம் செய்யும் வில்லைகளின் அதிகபட்சத் தொகை 1018 ஆக இருக்கும் என்றும் வைத்துக்கொண்டால், நமக்கு அதிகபட்சத் தொகையாக 1036 கிடைக்கும். 25615 > 1036, எனவே பதினைந்து பைட்டுகள் போதுமானது.
L1-இல் 160 எரிவாயு வீணாவது பொதுவாகப் புறக்கணிக்கத்தக்கது. ஒரு பரிவர்த்தனைக்குக் குறைந்தது 21,000 எரிவாயு (opens in a new tab) செலவாகும், எனவே கூடுதலாக 0.8% என்பது ஒரு பொருட்டல்ல.
இருப்பினும், L2-இல், நிலைமை வேறு. பரிவர்த்தனையின் முழுச் செலவும் கிட்டத்தட்ட அதை L1-இல் எழுதுவதற்கே ஆகிறது.
பரிவர்த்தனை அழைப்புத் தரவுக்குக் கூடுதலாக, 109 பைட்டுகள் பரிவர்த்தனைத் தலைப்பு (இலக்கு முகவரி, கையொப்பம் போன்றவை) உள்ளன.
எனவே மொத்தச் செலவு 109*16+576+160=2480 ஆகும், மேலும் அதில் சுமார் 6.5%-ஐ நாம் வீணாக்குகிறோம்.
இலக்கைக் கட்டுப்படுத்த முடியாதபோது செலவுகளைக் குறைத்தல்
இலக்கு ஒப்பந்தத்தின் மீது உங்களுக்குக் கட்டுப்பாடு இல்லை என்று வைத்துக்கொண்டால், நீங்கள் இன்னும் இதைப் போன்ற (opens in a new tab) ஒரு தீர்வைப் பயன்படுத்தலாம். தொடர்புடைய கோப்புகளைப் பார்ப்போம்.
Token.sol
இதுதான் இலக்கு ஒப்பந்தம் (opens in a new tab).
இது ஒரு கூடுதல் அம்சத்தைக் கொண்ட நிலையான ERC-20 ஒப்பந்தமாகும்.
இந்த faucet செயல்பாடு எந்தவொரு பயனரும் பயன்படுத்துவதற்குச் சில வில்லைகளைப் பெற அனுமதிக்கிறது.
இது ஒரு தயாரிப்பு ERC-20 ஒப்பந்தத்தைப் பயனற்றதாக்கும், ஆனால் சோதனையை எளிதாக்குவதற்கு மட்டுமே ERC-20 இருக்கும்போது இது வேலையை எளிதாக்குகிறது.
/**
* @dev அழைப்பாளருக்கு விளையாடுவதற்கு 1000 வில்லைகளை வழங்குகிறது
*/
செயற்கூறு faucet() external {
_mint(msg.sender, 1000);
} // function faucet
CalldataInterpreter.sol
குறுகிய அழைப்புத் தரவுடன் பரிவர்த்தனைகள் அழைக்க வேண்டிய ஒப்பந்தம் இதுதான் (opens in a new tab). இதை வரி வரியாகப் பார்ப்போம்.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import { OrisUselessToken } from "./Token.sol";
அதை எவ்வாறு அழைப்பது என்பதை அறிய நமக்கு வில்லைச் செயல்பாடு தேவை.
ஒப்பந்தம் CalldataInterpreter {
OrisUselessToken public immutable token;
நாம் பதிலியாக (proxy) இருக்கும் வில்லையின் முகவரி.
/**
* @dev வில்லை முகவரியைக் குறிப்பிடவும்
* @param tokenAddr_ ERC-20 ஒப்பந்த முகவரி
*/
ஆக்கி(
address tokenAddr_
) {
token = OrisUselessToken(tokenAddr_);
} // constructor
நாம் குறிப்பிட வேண்டிய ஒரே அளவுரு வில்லை முகவரி மட்டுமே.
function calldataVal(uint startByte, uint length)
private pure returns (uint) {
அழைப்புத் தரவிலிருந்து ஒரு மதிப்பைப் படிக்கவும்.
uint _retVal;
require(length < 0x21,
"calldataVal length limit is 32 bytes");
require(length + startByte <= msg.data.length,
"calldataVal trying to read beyond calldatasize");
நாம் ஒரு 32-பைட் (256-பிட்) சொல்லை நினைவகத்தில் ஏற்றி, நமக்குத் தேவையான புலத்தின் பகுதியாக இல்லாத பைட்டுகளை அகற்றப் போகிறோம். இந்த அல்காரிதம் 32 பைட்டுகளுக்கு மேல் உள்ள மதிப்புகளுக்கு வேலை செய்யாது, மேலும் நிச்சயமாக அழைப்புத் தரவின் முடிவைத் தாண்டி நம்மால் படிக்க முடியாது. L1-இல் எரிவாயுவைச் சேமிக்க இந்தச் சோதனைகளைத் தவிர்ப்பது அவசியமாக இருக்கலாம், ஆனால் L2-இல் எரிவாயு மிகவும் மலிவானது, இது நாம் நினைக்கும் எந்தவொரு சரிபார்ப்புகளையும் செயல்படுத்துகிறது.
assembly {
_retVal := calldataload(startByte)
}
fallback()-க்கான அழைப்பிலிருந்து தரவை நாம் நகலெடுத்திருக்கலாம் (கீழே காண்க), ஆனால் EVM-இன் அசெம்பிளி மொழியான Yul (opens in a new tab)-ஐப் பயன்படுத்துவது எளிதானது.
இங்கே நாம் startByte முதல் startByte+31 வரையிலான பைட்டுகளை அடுக்கில் (stack) படிக்க CALLDATALOAD செயல்பாட்டுக் குறியீட்டைப் (opcode) (opens in a new tab) பயன்படுத்துகிறோம்.
பொதுவாக, Yul-இல் ஒரு செயல்பாட்டுக் குறியீட்டின் தொடரியல் <opcode name>(<first stack value, if any>,<second stack value, if any>...) ஆகும்.
_retVal = _retVal >> (256-length*8);
மிக முக்கியமான length பைட்டுகள் மட்டுமே புலத்தின் ஒரு பகுதியாகும், எனவே மற்ற மதிப்புகளை அகற்ற நாம் வலது-நகர்வு (right-shift) (opens in a new tab) செய்கிறோம்.
இது மதிப்பை புலத்தின் வலதுபுறத்திற்கு நகர்த்துவதன் கூடுதல் நன்மையைக் கொண்டுள்ளது, எனவே இது மதிப்பு பெருக்கல் 256ஏதோவொன்று என்பதற்குப் பதிலாக மதிப்பாகவே இருக்கும்.
return _retVal;
}
fallback() external {
Solidity ஒப்பந்தத்திற்கான அழைப்பு எந்தவொரு செயல்பாட்டுக் கையொப்பங்களுடனும் (function signatures) பொருந்தாதபோது, அது fallback() செயல்பாட்டை (opens in a new tab) அழைக்கிறது (ஒன்று இருப்பதாகக் கருதி).
CalldataInterpreter-ஐப் பொறுத்தவரை, வேறு எந்த external அல்லது public செயல்பாடுகளும் இல்லாததால் எந்தவொரு அழைப்பும் இங்கு வருகிறது.
uint _func;
_func = calldataVal(0, 1);
அழைப்புத் தரவின் முதல் பைட்டைப் படிக்கவும், இது செயல்பாட்டை நமக்குக் கூறுகிறது. ஒரு செயல்பாடு இங்கு கிடைக்காமல் இருப்பதற்கு இரண்டு காரணங்கள் உள்ளன:
pureஅல்லதுviewஆக உள்ள செயல்பாடுகள் நிலையை (state) மாற்றுவதில்லை மற்றும் எரிவாயு செலவாகாது (புறச்சங்கிலியில் அழைக்கப்படும்போது). அவற்றின் எரிவாயு செலவைக் குறைக்க முயற்சிப்பதில் எந்த அர்த்தமும் இல்லை.msg.sender(opens in a new tab)-ஐச் சார்ந்திருக்கும் செயல்பாடுகள்.msg.sender-இன் மதிப்பு அழைப்பாளராக இல்லாமல்,CalldataInterpreter-இன் முகவரியாக இருக்கும்.
துரதிர்ஷ்டவசமாக, ERC-20 விவரக்குறிப்புகளைப் பார்க்கும்போது (opens in a new tab), இது transfer என்ற ஒரு செயல்பாட்டை மட்டுமே விட்டுவிடுகிறது.
இது நமக்கு இரண்டு செயல்பாடுகளை மட்டுமே விட்டுவிடுகிறது: transfer (ஏனெனில் நாம் transferFrom-ஐ அழைக்கலாம்) மற்றும் faucet (ஏனெனில் நம்மை அழைத்தவருக்கு வில்லைகளைத் திரும்பப் பரிமாற்றம் செய்யலாம்).
// வில்லையின் நிலையை மாற்றும் முறைகளை இதைப் பயன்படுத்தி அழைக்கவும்
// அழைப்புத் தரவிலிருந்து தகவல்கள்
// faucet
if (_func == 1) {
அளவுருக்கள் இல்லாத faucet()-க்கான அழைப்பு.
token.faucet();
token.transfer(msg.sender,
token.balanceOf(address(this)));
}
நாம் token.faucet()-ஐ அழைத்த பிறகு நமக்கு வில்லைகள் கிடைக்கும். இருப்பினும், பதிலி ஒப்பந்தமாக (proxy contract), நமக்கு வில்லைகள் தேவை இல்லை.
நம்மை அழைத்த EOA (வெளிப்புறமாகச் சொந்தமான கணக்கு) அல்லது ஒப்பந்தத்திற்கு அது தேவை.
எனவே நம்மை அழைத்தவருக்கு நமது வில்லைகள் அனைத்தையும் பரிமாற்றம் செய்கிறோம்.
// பரிமாற்றம் (அதற்கான அனுமதித்தொகை நம்மிடம் உள்ளது என வைத்துக்கொள்வோம்)
if (_func == 2) {
வில்லைகளைப் பரிமாற்றம் செய்வதற்கு இரண்டு அளவுருக்கள் தேவை: இலக்கு முகவரி மற்றும் தொகை.
token.transferFrom(
msg.sender,
அழைப்பாளர்கள் தங்களுக்குச் சொந்தமான வில்லைகளைப் பரிமாற்றம் செய்ய மட்டுமே நாங்கள் அனுமதிக்கிறோம்
address(uint160(calldataVal(1, 20))),
இலக்கு முகவரி பைட் #1-இல் தொடங்குகிறது (பைட் #0 என்பது செயல்பாடு). ஒரு முகவரியாக, இது 20-பைட்டுகள் நீளமானது.
calldataVal(21, 2)
இந்தக் குறிப்பிட்ட ஒப்பந்தத்திற்கு, எவரும் பரிமாற்றம் செய்ய விரும்பும் அதிகபட்ச வில்லைகளின் எண்ணிக்கை இரண்டு பைட்டுகளுக்குள் (65536-க்கும் குறைவானது) பொருந்தும் என்று நாங்கள் கருதுகிறோம்.
);
}
ஒட்டுமொத்தமாக, ஒரு பரிமாற்றத்திற்கு 35 பைட்டுகள் அழைப்புத் தரவு தேவைப்படுகிறது:
| பிரிவு | நீளம் | பைட்டுகள் |
|---|---|---|
| செயல்பாடுத் தேர்வி | 1 | 0 |
| இலக்கு முகவரி | 32 | 1-32 |
| தொகை | 2 | 33-34 |
} // fallback
} // contract CalldataInterpreter
test.js
இந்த JavaScript அலகுச் சோதனை (unit test) (opens in a new tab) இந்த வழிமுறையை எவ்வாறு பயன்படுத்துவது (மற்றும் அது சரியாகச் செயல்படுகிறதா என்பதை எவ்வாறு சரிபார்ப்பது) என்பதைக் காட்டுகிறது. நீங்கள் chai (opens in a new tab) மற்றும் ethers (opens in a new tab)-ஐப் புரிந்துகொள்கிறீர்கள் என்று நான் கருதுகிறேன், மேலும் ஒப்பந்தத்திற்குக் குறிப்பாகப் பொருந்தும் பகுதிகளை மட்டுமே விளக்குகிறேன்.
const { expect } = require("chai");
describe("CalldataInterpreter", function () {
it("Should let us use tokens", async function () {
const Token = await ethers.getContractFactory("OrisUselessToken")
const token = await Token.deploy()
await token.deployed()
console.log("Token addr:", token.address)
const Cdi = await ethers.getContractFactory("CalldataInterpreter")
const cdi = await Cdi.deploy(token.address)
await cdi.deployed()
console.log("CalldataInterpreter addr:", cdi.address)
const signer = await ethers.getSigner()
இரண்டு ஒப்பந்தங்களையும் பயன்படுத்துவதன் மூலம் தொடங்குகிறோம்.
// விளையாடுவதற்கு வில்லைகளைப் பெறவும்
const faucetTx = {
பரிவர்த்தனைகளை உருவாக்க நாம் பொதுவாகப் பயன்படுத்தும் உயர்மட்டச் செயல்பாடுகளை (token.faucet() போன்றவை) பயன்படுத்த முடியாது, ஏனெனில் நாம் ABI-ஐப் பின்பற்றுவதில்லை.
அதற்குப் பதிலாக, நாமே பரிவர்த்தனையை உருவாக்கி பின்னர் அதை அனுப்ப வேண்டும்.
to: cdi.address,
data: "0x01"
பரிவர்த்தனைக்கு நாம் வழங்க வேண்டிய இரண்டு அளவுருக்கள் உள்ளன:
to, இலக்கு முகவரி. இது அழைப்புத் தரவு மொழிபெயர்ப்பாளர் (calldata interpreter) ஒப்பந்தமாகும்.data, அனுப்ப வேண்டிய அழைப்புத் தரவு. பாசெட் (faucet) அழைப்பைப் பொறுத்தவரை, தரவு ஒரு ஒற்றை பைட் ஆகும்,0x01.
}
await (await signer.sendTransaction(faucetTx)).wait()
நாம் ஏற்கனவே இலக்கைக் (faucetTx.to) குறிப்பிட்டுள்ளதாலும், பரிவர்த்தனையில் கையொப்பமிட வேண்டியிருப்பதாலும் கையொப்பமிடுபவரின் sendTransaction முறையை (opens in a new tab) அழைக்கிறோம்.
// faucet வில்லைகளைச் சரியாக வழங்குகிறதா எனச் சரிபார்க்கவும்
expect(await token.balanceOf(signer.address)).to.equal(1000)
இங்கே நாம் இருப்பைச் சரிபார்க்கிறோம்.
view செயல்பாடுகளில் எரிவாயுவைச் சேமிக்க வேண்டிய அவசியமில்லை, எனவே அவற்றை வழக்கம் போல் இயக்குகிறோம்.
// CDI-க்கு ஒரு அனுமதித்தொகையை வழங்கவும் (ஒப்புதல்களைப் பதிலாளாகச் செய்ய முடியாது)
const approveTX = await token.approve(cdi.address, 10000)
await approveTX.wait()
expect(await token.allowance(signer.address, cdi.address)).to.equal(10000)
பரிமாற்றங்களைச் செய்ய அழைப்புத் தரவு மொழிபெயர்ப்பாளருக்கு அனுமதித்தொகையை (allowance) வழங்கவும்.
// வில்லைகளைப் பரிமாற்றவும்
const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"
const transferTx = {
to: cdi.address,
data: "0x02" + destAddr.slice(2, 42) + "0100",
}
ஒரு பரிமாற்றப் பரிவர்த்தனையை உருவாக்கவும். முதல் பைட் "0x02", அதைத் தொடர்ந்து இலக்கு முகவரி, இறுதியாகத் தொகை (0x0100, இது தசமத்தில் 256 ஆகும்).
await (await signer.sendTransaction(transferTx)).wait()
// நம்மிடம் 256 வில்லைகள் குறைவாக உள்ளதா எனச் சரிபார்க்கவும்
expect (await token.balanceOf(signer.address)).to.equal(1000-256)
// மேலும் நமது இலக்கு அவற்றைப் பெற்றுள்ளதா எனவும் சரிபார்க்கவும்
expect (await token.balanceOf(destAddr)).to.equal(256)
}) // it
}) // describe
இலக்கு ஒப்பந்தத்தை நீங்கள் கட்டுப்படுத்தும்போது செலவைக் குறைத்தல்
இலக்கு ஒப்பந்தத்தின் மீது உங்களுக்குக் கட்டுப்பாடு இருந்தால், அழைப்புத் தரவு மொழிபெயர்ப்பாளரை நம்புவதால் msg.sender சரிபார்ப்புகளைத் தவிர்க்கும் செயல்பாடுகளை நீங்கள் உருவாக்கலாம்.
இது எவ்வாறு செயல்படுகிறது என்பதற்கான எடுத்துக்காட்டை இங்கே, control-contract கிளையில் பார்க்கலாம் (opens in a new tab).
ஒப்பந்தம் வெளிப்புறப் பரிவர்த்தனைகளுக்கு மட்டுமே பதிலளிப்பதாக இருந்தால், ஒரே ஒரு ஒப்பந்தத்தை வைத்திருப்பதன் மூலம் நாம் சமாளிக்க முடியும். இருப்பினும், அது ஒருங்கிணைக்கக்கூடிய தன்மையை (composability) உடைத்துவிடும். வழக்கமான ERC-20 அழைப்புகளுக்குப் பதிலளிக்கும் ஒரு ஒப்பந்தத்தையும், குறுகிய அழைப்புத் தரவுடன் பரிவர்த்தனைகளுக்குப் பதிலளிக்கும் மற்றொரு ஒப்பந்தத்தையும் வைத்திருப்பது மிகவும் சிறந்தது.
Token.sol
இந்த எடுத்துக்காட்டில் நாம் Token.sol-ஐ மாற்றலாம்.
பதிலி மட்டுமே அழைக்கக்கூடிய பல செயல்பாடுகளை வைத்திருக்க இது நம்மை அனுமதிக்கிறது.
புதிய பகுதிகள் இங்கே:
// CalldataInterpreter முகவரியைக் குறிப்பிட அனுமதிக்கப்பட்ட ஒரே முகவரி
address owner;
// CalldataInterpreter முகவரி
address proxy = address(0);
அங்கீகரிக்கப்பட்ட பதிலியின் அடையாளத்தை ERC-20 ஒப்பந்தம் அறிய வேண்டும். இருப்பினும், இந்த மாறியை ஆக்கியில் (constructor) அமைக்க முடியாது, ஏனெனில் அதன் மதிப்பு நமக்கு இன்னும் தெரியாது. பதிலி அதன் ஆக்கியில் வில்லையின் முகவரியை எதிர்பார்ப்பதால் இந்த ஒப்பந்தம் முதலில் தொடங்கப்படுகிறது.
/**
* @dev ERC20 ஆக்கியை அழைக்கிறது.
*/
constructor(
) ERC20("Oris useless token-2", "OUT-2") {
owner = msg.sender;
}
உருவாக்கியவரின் முகவரி (owner என அழைக்கப்படுகிறது) இங்கே சேமிக்கப்படுகிறது, ஏனெனில் பதிலியை அமைக்க அனுமதிக்கப்பட்ட ஒரே முகவரி அதுதான்.
/**
* @dev பதிலாளுக்கான (CalldataInterpreter) முகவரியை அமைக்கவும்.
* உரிமையாளரால் ஒரு முறை மட்டுமே அழைக்கப்பட முடியும்
*/
செயற்கூறு setProxy(address _proxy) external {
require(msg.sender == owner, "Can only be called by owner");
require(proxy == address(0), "Proxy is already set");
proxy = _proxy;
} // function setProxy
பதிலிக்குச் சிறப்புரிமை அணுகல் உள்ளது, ஏனெனில் அது பாதுகாப்புச் சரிபார்ப்புகளைத் தவிர்க்க முடியும்.
பதிலியை நாம் நம்பலாம் என்பதை உறுதிப்படுத்த, owner-ஐ மட்டுமே இந்தச் செயல்பாட்டை அழைக்க அனுமதிக்கிறோம், அதுவும் ஒரு முறை மட்டுமே.
ஒருமுறை proxy உண்மையான மதிப்பைப் (பூஜ்ஜியம் அல்ல) பெற்றவுடன், அந்த மதிப்பை மாற்ற முடியாது, எனவே உரிமையாளர் மோசடி செய்ய முடிவு செய்தாலும் அல்லது அதற்கான நினைவூட்டி (mnemonic) வெளிப்படுத்தப்பட்டாலும், நாம் இன்னும் பாதுகாப்பாகவே இருக்கிறோம்.
/**
* @dev சில செயற்கூறுகள் பதிலாளால் மட்டுமே அழைக்கப்படலாம்.
*/
modifier onlyProxy {
இது ஒரு modifier செயல்பாடு (opens in a new tab), இது மற்ற செயல்பாடுகள் வேலை செய்யும் முறையை மாற்றியமைக்கிறது.
require(msg.sender == proxy);
முதலில், நாம் பதிலியால் அழைக்கப்பட்டோம், வேறு எவராலும் அல்ல என்பதைச் சரிபார்க்கவும்.
இல்லையெனில், revert.
_;
}
அப்படியானால், நாம் மாற்றியமைக்கும் செயல்பாட்டை இயக்கவும்.
/* கணக்குகளுக்காகப் பதிலாள் உண்மையாகவே பதிலாளாகச் செயல்பட அனுமதிக்கும் செயற்கூறுகள் */
function transferProxy(address from, address to, uint256 amount)
public virtual onlyProxy() returns (bool)
{
_transfer(from, to, amount);
return true;
}
function approveProxy(address from, address spender, uint256 amount)
public virtual onlyProxy() returns (bool)
{
_approve(from, spender, amount);
return true;
}
function transferFromProxy(
address spender,
address from,
address to,
uint256 amount
) public virtual onlyProxy() returns (bool)
{
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
இவை பொதுவாக வில்லைகளைப் பரிமாற்றம் செய்யும் அல்லது அனுமதித்தொகையை அங்கீகரிக்கும் நிறுவனத்திடமிருந்து நேரடியாகச் செய்தி (message) வர வேண்டிய மூன்று செயல்பாடுகளாகும். இங்கே இந்தச் செயல்பாடுகளின் பதிலிப் பதிப்பு நம்மிடம் உள்ளது, இது:
onlyProxy()மூலம் மாற்றியமைக்கப்பட்டுள்ளது, எனவே வேறு எவரும் அவற்றைக் கட்டுப்படுத்த அனுமதிக்கப்பட மாட்டார்கள்.- பொதுவாக
msg.senderஆக இருக்கும் முகவரியைக் கூடுதல் அளவுருவாகப் பெறுகிறது.
CalldataInterpreter.sol
பதிலிச் செயல்பாடுகள் msg.sender அளவுருவைப் பெறுகின்றன மற்றும் transfer-க்கு அனுமதித்தொகை தேவையில்லை என்பதைத் தவிர, அழைப்புத் தரவு மொழிபெயர்ப்பாளர் மேலே உள்ளதைப் போலவே இருக்கும்.
// பரிமாற்றம் (அனுமதித்தொகை தேவையில்லை)
if (_func == 2) {
token.transferProxy(
msg.sender,
address(uint160(calldataVal(1, 20))),
calldataVal(21, 2)
);
}
// approve
if (_func == 3) {
token.approveProxy(
msg.sender,
address(uint160(calldataVal(1, 20))),
calldataVal(21, 2)
);
}
// transferFrom
if (_func == 4) {
token.transferFromProxy(
msg.sender,
address(uint160(calldataVal( 1, 20))),
address(uint160(calldataVal(21, 20))),
calldataVal(41, 2)
);
}
Test.js
முந்தைய சோதனைக் குறியீட்டிற்கும் இதற்கும் இடையே சில மாற்றங்கள் உள்ளன.
const Cdi = await ethers.getContractFactory("CalldataInterpreter")
const cdi = await Cdi.deploy(token.address)
await cdi.deployed()
await token.setProxy(cdi.address)
எந்தப் பதிலியை நம்ப வேண்டும் என்பதை ERC-20 ஒப்பந்தத்திற்கு நாம் கூற வேண்டும்
console.log("CalldataInterpreter addr:", cdi.address)
// அனுமதித்தொகைகளைச் சரிபார்க்க இரண்டு கையொப்பமிடுபவர்கள் தேவை
const signers = await ethers.getSigners()
const signer = signers[0]
const poorSigner = signers[1]
approve() மற்றும் transferFrom()-ஐச் சரிபார்க்க நமக்கு இரண்டாவது கையொப்பமிடுபவர் தேவை.
இது நமது வில்லைகள் எதையும் பெறாததால் (நிச்சயமாக இதற்கு ETH தேவை) இதை poorSigner என்று அழைக்கிறோம்.
// வில்லைகளைப் பரிமாற்றவும்
const destAddr = "0xf5a6ead936fb47f342bb63e676479bddf26ebe1d"
const transferTx = {
to: cdi.address,
data: "0x02" + destAddr.slice(2, 42) + "0100",
}
await (await signer.sendTransaction(transferTx)).wait()
ERC-20 ஒப்பந்தம் பதிலியை நம்புவதால் (cdi), பரிமாற்றங்களைச் செய்ய நமக்கு அனுமதித்தொகை தேவையில்லை.
// ஒப்புதல் மற்றும் transferFrom
const approveTx = {
to: cdi.address,
data: "0x03" + poorSigner.address.slice(2, 42) + "00FF",
}
await (await signer.sendTransaction(approveTx)).wait()
const destAddr2 = "0xE1165C689C0c3e9642cA7606F5287e708d846206"
const transferFromTx = {
to: cdi.address,
data: "0x04" + signer.address.slice(2, 42) + destAddr2.slice(2, 42) + "00FF",
}
await (await poorSigner.sendTransaction(transferFromTx)).wait()
// approve / transferFrom இணைப்பு சரியாகச் செய்யப்பட்டதா எனச் சரிபார்க்கவும்
expect(await token.balanceOf(destAddr2)).to.equal(255)
இரண்டு புதிய செயல்பாடுகளைச் சோதிக்கவும்.
transferFromTx-க்கு இரண்டு முகவரி அளவுருக்கள் தேவை என்பதை நினைவில் கொள்ளவும்: அனுமதித்தொகையை வழங்குபவர் மற்றும் பெறுபவர்.
முடிவுரை
ஆப்டிமிசம் (opens in a new tab) மற்றும் ஆர்பிட்ரம் (opens in a new tab) ஆகிய இரண்டும் L1-இல் எழுதப்பட்ட அழைப்புத் தரவின் அளவைக் குறைப்பதற்கான வழிகளைத் தேடுகின்றன, எனவே பரிவர்த்தனைகளின் செலவையும் குறைக்கின்றன. இருப்பினும், பொதுவான தீர்வுகளைத் தேடும் உள்கட்டமைப்பு வழங்குநர்களாக, எங்கள் திறன்கள் வரம்புக்குட்பட்டவை. பரவலாக்கப்பட்ட செயலி (dapp) உருவாக்குநராக, உங்களிடம் பயன்பாடு சார்ந்த அறிவு உள்ளது, இது ஒரு பொதுவான தீர்வில் எங்களால் முடிந்ததை விட உங்கள் அழைப்புத் தரவைச் சிறப்பாக உகப்பாக்க உங்களை அனுமதிக்கிறது. இந்தக் கட்டுரை உங்கள் தேவைகளுக்கான சிறந்த தீர்வைக் கண்டறிய உதவும் என்று நம்புகிறோம்.