-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev/composability solidity libraries #54
base: main
Are you sure you want to change the base?
Conversation
…lidity logic and test script to transfer tokens and updated README
…ated testCreateInitializeATA authentication logic and updated README
…nstrate creating ATAs for third party accounts
…dity logic and test script to revoke ATA delegation and updated README
@maxpolizzo a question for my own understanding - why did you leave the library methods as |
I did so assuming that these libraries would be deployed once and for all and used as "linked libraries" by other contracts. Maybe it is not the case, they could also be used as "embedded libraries" in which case their functions should be internal. |
For the initial version let's keep them embedded |
…Composability.getAssociatedTokenAccount() and added ATANonce, tokenMintSeed and tokenMintDecimals to config.js file
/// @notice Helper library for interactions with Solana's System program | ||
/// @author [email protected] | ||
library LibSystemProgram { | ||
bytes32 public constant SYSTEM_PROGRAM_ID = 0x0000000000000000000000000000000000000000000000000000000000000000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better readable code we can change this to bytes32(0)
/// @notice Helper function to format a uint256 value as right-padded little-endian bytes | ||
/// @param bigEndian The uint256 value to be formatted | ||
/// @return littleEndian The same value formatted as a right-padded little-endian bytes32 | ||
function convertUintToLittleEndianBytes32(uint256 bigEndian) public pure returns (bytes32 littleEndian) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This library is not needed as we already have this functionality here. Loops always require a bit heavier gas cost execution, here is quick report of comparing the 2 approaches:
function convertUintToLittleEndianBytes32(uint256 bigEndian) public pure returns (bytes32 littleEndian) {
assembly {
for {let i := 0} lt(i, 32) {i := add(i, 1)} {
let nextBEByte := byte(sub(31, i), bigEndian) // Get BE bytes starting from the right
let nextLEByte := shl(sub(248, mul(i, 8)), nextBEByte) // Shift left to get LE bytes
littleEndian := add(littleEndian, nextLEByte) // Add LE bytes to return value
}
}
}
// execution cost - 3515 gas
function toBytes8(uint256 input) public pure returns (bytes8) {
return bytes8(convertUintToLittleEndianBytes32(input));
}
function readLittleEndianSigned256(uint256 input) public pure returns (int256) {
input = ((input << 8) & 0xFF00FF00FF00FF00FF00FF00FF00FF00) | ((input >> 8) & 0x00FF00FF00FF00FF00FF00FF00FF00FF);
input = ((input << 16) & 0xFFFF0000FFFF0000FFFF0000FFFF0000) | ((input >> 16) & 0x0000FFFF0000FFFF0000FFFF0000FFFF);
input = ((input << 32) & 0xFFFFFFFF00000000FFFFFFFF00000000) | ((input >> 32) & 0x00000000FFFFFFFF00000000FFFFFFFF);
input = ((input << 64) & 0xFFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF0000000000000000) | ((input >> 64) & 0x0000000000000000FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF);
return int256((input << 128) | ((input >> 128) & 0xFFFFFFFFFFFFFFFF));
}
function _uint256ToBytes8(uint256 input) internal pure returns(bytes8) {
bytes8 result;
int256 value = readLittleEndianSigned256(input);
assembly {
result := value // Implicit truncation to the lower 8 bytes
}
return result;
}
// execution cost - 624 gas
function toBytes8Optimized(uint256 input) public pure returns (bytes8) {
return _uint256ToBytes8(input);
}
Method toBytes8
takes 3515 gas meanwhile toBytes8Optimized
takes nearly 5 times lesser gas usage.
) { | ||
accounts = new bytes32[](3); | ||
accounts[0] = payer; | ||
accounts[1] = sha256(abi.encodePacked(basePubKey, seed, programId)); // Account to be created |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you have the same logic, I advise you to add it into an internal method and call it here and inside the public method getCreateWithSeedAccount
bytes32 user = CALL_SOLANA.getNeonAddress(msg.sender); | ||
// Authentication: we derive the user's associated token account from the user account, the token mint account | ||
// and the nonce that was used to create the user's ATA through this contract | ||
bytes32 userATA = CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ATA calculation can be moved inside LibSPLTokenProgram.formatRevokeInstruction
and this method can accept the_tokenMint
parameter instead the userATA
. The idea behind this is to abstract away as much as possible Solana logic from the EVM developer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I'm confused why do you need the ATA nonce at all? Here is quick way how to get an user's ATA on-chain:
function getAssociateTokenAccount(bytes32 solanaAccount, bytes32 tokenMint) public view returns (bytes32) {
return CALL_SOLANA.getSolanaPDA(
ASSOCIATED_TOKEN_PROGRAM,
abi.encodePacked(
solanaAccount,
TOKEN_PROGRAM,
tokenMint
)
);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's also good idea to include this getter inside LibSPLTokenProgram.sol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getAssociatedTokenAccount
function implemented in TestComposability contract and getSolanaPDA
function from the CALL_SOLANA precompile don’t return the same pubKey.
For instance:
3iSkPAZeSaKQLy3TzVec3VNsc8zc8yvJ5XvJm5bArAGo
<-- ATA derived by getAssociatedTokenAccount
function with nonce == 0
ApeW2S6fEjfcHF8KLjaK6NsRrdRTKsMJ7QHDhXbpSHEi
<-- PDA derived from the same account by getSolanaPDA
function
The nonce
is a “bump seed” used to ensure that PDA falls off-curve. See here.
Here we added a note saying that getSolanaPDA
uses the same logic as findProgramAddressSeed implementation in @solana/web3.js npm module but findProgramAddressSeed
does return the nonce
used to derive the ATA, while getSolanaPDA
does not.
I guess getSolanaPDA
still iterates a nonce
under the hood to derive an off-curve PDA, but it doesn't give the option to use different nonces.
The reason why I left the option to choose the nonce
is that
in the specific case of ATAs the nonce
can be used to generate several different ATAs for the same account . This could be useful depending on the use case (for instance when using bots or when managing many accounts belonging to different users, having different ATAs allows to easily keep track of balances). If we don't care about this then we could use getSolanaPDA
function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I understand your point, imo this logic with querying ATAs by nonces is not needed now, but it could be needed on a later stage for some integration. I'd suggest make one private method in the LibSPLTokenProgram.sol
and 2 internal methods that point to it:
- first internal method does pretty much the same thing as
getAssociatedTokenAccount
( under the hood querying ATA with nonce 0, but not bothering the user to worry about nonce ) - second internal method is to query ATA by given
nonce
parameter
But please move both of these methods inside LibSPLTokenProgram.sol
and TestComposability.sol
should be calling them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the following functions to LibSPLTokenProgram library:
function getAssociatedTokenAccount(
bytes32 _tokenMint,
bytes32 userPubKey
) internal view returns(bytes32) {
// Returns ATA derived with nonce == 0 by default
return _getAssociatedTokenAccount(_tokenMint, userPubKey, 0);
}
function getAssociatedTokenAccount(
bytes32 _tokenMint,
bytes32 userPubKey,
uint8 nonce
) internal view returns(bytes32) {
return _getAssociatedTokenAccount(_tokenMint, userPubKey, nonce);
}
function _getAssociatedTokenAccount(
bytes32 _tokenMint,
bytes32 userPubKey,
uint8 nonce
) private view returns(bytes32) {
return CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked(
userPubKey,
LibSPLTokenProgram.TOKEN_PROGRAM_ID,
_tokenMint,
nonce,
LibSPLTokenProgram.ASSOCIATED_TOKEN_PROGRAM_ID
)));
}
And the following function to TestComposability contract:
function getAssociatedTokenAccount(
bytes32 _tokenMint,
bytes32 userPubKey
) public view returns(bytes32) {
// Returns ATA derived with nonce == 0 by default
return LibSPLTokenProgram.getAssociatedTokenAccount(_tokenMint, userPubKey);
}
Regarding the default nonce value, here we use 0 but the @solana/spl-token NPM library starts with 255 and iterates down until it finds an ATA that is off-curve (this is the canonical way of deriving ATAs). So I'm wondering if we shouldn't use 255 as the default value maybe?
// This contract is mint/freeze authority | ||
bytes32 mintAuthority = CALL_SOLANA.getNeonAddress(address(this)); | ||
// Authentication: we derive token mint account from msg.sender and seed | ||
bytes32 _tokenMint = CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_tokenMint
can be moved inside LibSPLTokenProgram.formatMintToInstruction
. Again the idea behind this is to not bother too much the EVM dev with Solana stuff
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The specification for those libraries was to safely format instructions for a specific Solana program, but ATA and token mint derivations involve some authentication logic when deriving the salt
value passed to CALL_SOLANA's getResourceAddress
function.
In the TestComposability example this authentication uses msg.sender
and a seed
(for token mint derivation) or a nonce
(for ata derivation).
If we move the salt
derivation inside the library then devs using the library won't have a choice on the authentication logic: maybe they would like to use different parameters to derive the salt
(based on their use case).
Maybe they would like to use tx.origin
instead of msg.sender
(if they are using another intermediary contract to call the library method for instance), or maybe they would like to have an additional seed involved in the salt
calculation.
If we add too much logic into a single library we lose modularity, which increases the likelihood that devs will re-write their own library to fit their use case. It would also be possible to have another library for authentication on top of the library that formats instructions, which would keep each library modular.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well argumented, I agree with you here. Please just make it clear through comments that it's entirely possible to change the _tokenMint
calculation into whatever they desire. For example it could be something else rather than using msg.sender or tx.origin - could be anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added the following function to TestComposability contract:
function getTokenMintAccount(bytes memory seed) public view returns(bytes32) {
// Returns the token mint account derived from from msg.sender and seed
return CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked(
msg.sender, // msg.sender is included here for authentication
seed // Seed that has been used to create token mint
)));
}
And the following comments to testCreateInitializeTokenMint
function in the same contract:
// Create SPL token mint account: msg.sender and a seed are used to calculate the salt used to derive the token
// mint account, allowing for future authentication when interacting with this token mint. Note that it is
// entirely possible to calculate the salt in a different manner and to use a different approach for
// authentication
tokenMint = CALL_SOLANA.createResource(
sha256(abi.encodePacked(
msg.sender, // msg.sender is included here for future authentication
seed // using different seeds allows msg.sender to create different token mint accounts
)), // salt
LibSPLTokenProgram.MINT_SIZE, // space
LibSPLTokenProgram.MINT_RENT_EXEMPT_BALANCE, // lamports
LibSPLTokenProgram.TOKEN_PROGRAM_ID // Owner must be SPL Token program
);
// This contract is the current mint authority | ||
bytes32 currentAuthority = CALL_SOLANA.getNeonAddress(address(this)); | ||
// Authentication: we derive token mint account from msg.sender and seed | ||
bytes32 _tokenMint = CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls move _tokenMint
inside LibSPLTokenProgram.formatUpdateMintAuthorityInstruction
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
bytes32 sender = CALL_SOLANA.getNeonAddress(msg.sender); | ||
// Authentication: we derive the sender's associated token account from the sender account, the token mint account and the nonce | ||
// that was used to create the sender's associated token account through this contract | ||
bytes32 senderATA = CALL_SOLANA.getResourceAddress(sha256(abi.encodePacked( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls move senderATA
inside LibSPLTokenProgram.formatTransferInstruction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
tokenOwner = CALL_SOLANA.getNeonAddress(address(this)); | ||
} | ||
// Create SPL associated token account | ||
ata = CALL_SOLANA.createResource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls move ata
inside LibSPLTokenProgram.formatInitializeAccount2Instruction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
|
||
function testCreateInitializeTokenMint(bytes memory seed, uint8 decimals) external { | ||
// Create SPL token mint account | ||
tokenMint = CALL_SOLANA.createResource( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pls move tokenMint
inside LibSPLTokenProgram.formatInitializeMint2Instruction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
return LibSystemProgram.getCreateWithSeedAccount(basePubKey, programId, seed); | ||
} | ||
|
||
function getAssociatedTokenAccount( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method can be removed, in another comment I did share ATA account calculation without the need of passing nonce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed here
…iatedTokenAccount functions to LibSPLTokenProgram library, added getTokenMintAccount function to testComposability contract, using readLittleEndianUnsigned256 function from SolanaDataConverterLib library and implemented other suggestions from review
This PR contains the following:
To do:
internal
(to use as embedded libraries)TestComposability.ata()
from demo scripts, replace with calls toTestComposability.getAssociatedTokenAccount()
Run with:
npx hardhat run ./scripts/TestComposability/test.js --network curvestand